detectkit 0.2.4__py3-none-any.whl
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/__init__.py +17 -0
- detectkit/alerting/__init__.py +13 -0
- detectkit/alerting/channels/__init__.py +21 -0
- detectkit/alerting/channels/base.py +193 -0
- detectkit/alerting/channels/email.py +146 -0
- detectkit/alerting/channels/factory.py +193 -0
- detectkit/alerting/channels/mattermost.py +53 -0
- detectkit/alerting/channels/slack.py +55 -0
- detectkit/alerting/channels/telegram.py +110 -0
- detectkit/alerting/channels/webhook.py +139 -0
- detectkit/alerting/orchestrator.py +369 -0
- detectkit/cli/__init__.py +1 -0
- detectkit/cli/commands/__init__.py +1 -0
- detectkit/cli/commands/init.py +282 -0
- detectkit/cli/commands/run.py +486 -0
- detectkit/cli/commands/test_alert.py +184 -0
- detectkit/cli/main.py +186 -0
- detectkit/config/__init__.py +30 -0
- detectkit/config/metric_config.py +499 -0
- detectkit/config/profile.py +285 -0
- detectkit/config/project_config.py +164 -0
- detectkit/config/validator.py +124 -0
- detectkit/core/__init__.py +6 -0
- detectkit/core/interval.py +132 -0
- detectkit/core/models.py +106 -0
- detectkit/database/__init__.py +27 -0
- detectkit/database/clickhouse_manager.py +393 -0
- detectkit/database/internal_tables.py +724 -0
- detectkit/database/manager.py +324 -0
- detectkit/database/tables.py +138 -0
- detectkit/detectors/__init__.py +6 -0
- detectkit/detectors/base.py +441 -0
- detectkit/detectors/factory.py +138 -0
- detectkit/detectors/statistical/__init__.py +8 -0
- detectkit/detectors/statistical/iqr.py +508 -0
- detectkit/detectors/statistical/mad.py +478 -0
- detectkit/detectors/statistical/manual_bounds.py +206 -0
- detectkit/detectors/statistical/zscore.py +491 -0
- detectkit/loaders/__init__.py +6 -0
- detectkit/loaders/metric_loader.py +470 -0
- detectkit/loaders/query_template.py +164 -0
- detectkit/orchestration/__init__.py +9 -0
- detectkit/orchestration/task_manager.py +746 -0
- detectkit/utils/__init__.py +17 -0
- detectkit/utils/stats.py +196 -0
- detectkit-0.2.4.dist-info/METADATA +237 -0
- detectkit-0.2.4.dist-info/RECORD +51 -0
- detectkit-0.2.4.dist-info/WHEEL +5 -0
- detectkit-0.2.4.dist-info/entry_points.txt +2 -0
- detectkit-0.2.4.dist-info/licenses/LICENSE +21 -0
- detectkit-0.2.4.dist-info/top_level.txt +1 -0
detectkit/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
detectk - Anomaly Detection for Time-Series Metrics
|
|
3
|
+
|
|
4
|
+
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from detectkit.core.interval import Interval
|
|
10
|
+
from detectkit.core.models import ColumnDefinition, TableModel
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Interval",
|
|
14
|
+
"ColumnDefinition",
|
|
15
|
+
"TableModel",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Alert channels for external notifications."""
|
|
2
|
+
|
|
3
|
+
from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
|
|
4
|
+
from detectkit.alerting.channels.mattermost import MattermostChannel
|
|
5
|
+
from detectkit.alerting.channels.slack import SlackChannel
|
|
6
|
+
from detectkit.alerting.channels.webhook import WebhookChannel
|
|
7
|
+
from detectkit.alerting.channels.telegram import TelegramChannel
|
|
8
|
+
from detectkit.alerting.channels.email import EmailChannel
|
|
9
|
+
|
|
10
|
+
from detectkit.alerting.channels.factory import AlertChannelFactory
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AlertData",
|
|
14
|
+
"BaseAlertChannel",
|
|
15
|
+
"WebhookChannel",
|
|
16
|
+
"MattermostChannel",
|
|
17
|
+
"SlackChannel",
|
|
18
|
+
"TelegramChannel",
|
|
19
|
+
"EmailChannel",
|
|
20
|
+
"AlertChannelFactory",
|
|
21
|
+
]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base alert channel interface.
|
|
3
|
+
|
|
4
|
+
All alert channels must inherit from BaseAlertChannel and implement
|
|
5
|
+
the send() method for delivering alerts to specific destinations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from detectkit.detectors.base import DetectionResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class AlertData:
|
|
17
|
+
"""
|
|
18
|
+
Data for alert message.
|
|
19
|
+
|
|
20
|
+
Contains all information needed to format and send an alert.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
metric_name: Name of the metric
|
|
24
|
+
timestamp: Timestamp of the anomaly (datetime64)
|
|
25
|
+
timezone: Timezone for display (e.g., "Europe/Moscow")
|
|
26
|
+
value: Actual metric value
|
|
27
|
+
confidence_lower: Lower confidence bound
|
|
28
|
+
confidence_upper: Upper confidence bound
|
|
29
|
+
detector_name: Name/ID of detector that found the anomaly
|
|
30
|
+
detector_params: Detector parameters (JSON string)
|
|
31
|
+
direction: Direction of anomaly ("above" or "below")
|
|
32
|
+
severity: Severity score
|
|
33
|
+
detection_metadata: Additional metadata from detector
|
|
34
|
+
consecutive_count: Number of consecutive anomalies
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
metric_name: str
|
|
38
|
+
timestamp: Any # datetime64 or datetime
|
|
39
|
+
timezone: str
|
|
40
|
+
value: float
|
|
41
|
+
confidence_lower: Optional[float]
|
|
42
|
+
confidence_upper: Optional[float]
|
|
43
|
+
detector_name: str
|
|
44
|
+
detector_params: str
|
|
45
|
+
direction: str
|
|
46
|
+
severity: float
|
|
47
|
+
detection_metadata: Dict[str, Any]
|
|
48
|
+
consecutive_count: int = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BaseAlertChannel(ABC):
|
|
52
|
+
"""
|
|
53
|
+
Abstract base class for alert channels.
|
|
54
|
+
|
|
55
|
+
Alert channels deliver notifications to external systems when
|
|
56
|
+
anomalies are detected. Each channel implements a specific
|
|
57
|
+
delivery mechanism (webhook, email, etc.).
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> class MyChannel(BaseAlertChannel):
|
|
61
|
+
... def send(self, alert_data, template=None):
|
|
62
|
+
... message = self.format_message(alert_data, template)
|
|
63
|
+
... # Send via specific mechanism
|
|
64
|
+
... return True
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def send(
|
|
69
|
+
self,
|
|
70
|
+
alert_data: AlertData,
|
|
71
|
+
template: Optional[str] = None,
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Send alert to this channel.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
alert_data: Alert data to send
|
|
78
|
+
template: Optional custom message template
|
|
79
|
+
Uses default template if None
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if sent successfully, False otherwise
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
Exception: If sending fails critically
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> alert = AlertData(
|
|
89
|
+
... metric_name="cpu_usage",
|
|
90
|
+
... timestamp=datetime.now(),
|
|
91
|
+
... value=95.0,
|
|
92
|
+
... ...
|
|
93
|
+
... )
|
|
94
|
+
>>> success = channel.send(alert)
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def format_message(
|
|
99
|
+
self,
|
|
100
|
+
alert_data: AlertData,
|
|
101
|
+
template: Optional[str] = None,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Format alert message from template.
|
|
105
|
+
|
|
106
|
+
Uses default template if none provided. Template variables:
|
|
107
|
+
- {metric_name}
|
|
108
|
+
- {timestamp}
|
|
109
|
+
- {timezone}
|
|
110
|
+
- {value}
|
|
111
|
+
- {confidence_lower}
|
|
112
|
+
- {confidence_upper}
|
|
113
|
+
- {detector_name}
|
|
114
|
+
- {direction}
|
|
115
|
+
- {severity}
|
|
116
|
+
- {consecutive_count}
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
alert_data: Alert data to format
|
|
120
|
+
template: Optional custom template string
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Formatted message string
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> template = "Anomaly in {metric_name}: {value}"
|
|
127
|
+
>>> message = channel.format_message(alert_data, template)
|
|
128
|
+
"""
|
|
129
|
+
if template is None:
|
|
130
|
+
template = self.get_default_template()
|
|
131
|
+
|
|
132
|
+
# Format timestamp to string
|
|
133
|
+
from datetime import datetime
|
|
134
|
+
import numpy as np
|
|
135
|
+
|
|
136
|
+
ts = alert_data.timestamp
|
|
137
|
+
if isinstance(ts, np.datetime64):
|
|
138
|
+
ts = ts.astype(datetime)
|
|
139
|
+
|
|
140
|
+
# Format timestamp with timezone
|
|
141
|
+
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
|
|
142
|
+
if alert_data.timezone:
|
|
143
|
+
ts_str = f"{ts_str} ({alert_data.timezone})"
|
|
144
|
+
|
|
145
|
+
# Format confidence interval
|
|
146
|
+
if alert_data.confidence_lower is not None and alert_data.confidence_upper is not None:
|
|
147
|
+
confidence_str = f"[{alert_data.confidence_lower:.2f}, {alert_data.confidence_upper:.2f}]"
|
|
148
|
+
else:
|
|
149
|
+
confidence_str = "N/A"
|
|
150
|
+
|
|
151
|
+
# Format message
|
|
152
|
+
try:
|
|
153
|
+
message = template.format(
|
|
154
|
+
metric_name=alert_data.metric_name,
|
|
155
|
+
timestamp=ts_str,
|
|
156
|
+
timezone=alert_data.timezone,
|
|
157
|
+
value=alert_data.value,
|
|
158
|
+
confidence_lower=alert_data.confidence_lower,
|
|
159
|
+
confidence_upper=alert_data.confidence_upper,
|
|
160
|
+
confidence_interval=confidence_str,
|
|
161
|
+
detector_name=alert_data.detector_name,
|
|
162
|
+
detector_params=alert_data.detector_params,
|
|
163
|
+
direction=alert_data.direction,
|
|
164
|
+
severity=alert_data.severity,
|
|
165
|
+
consecutive_count=alert_data.consecutive_count,
|
|
166
|
+
)
|
|
167
|
+
except KeyError as e:
|
|
168
|
+
# If template has unknown variables, fall back to default
|
|
169
|
+
message = self.format_message(alert_data, self.get_default_template())
|
|
170
|
+
|
|
171
|
+
return message
|
|
172
|
+
|
|
173
|
+
def get_default_template(self) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Get default message template.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Default template string
|
|
179
|
+
"""
|
|
180
|
+
return (
|
|
181
|
+
"Anomaly detected in metric: {metric_name}\n"
|
|
182
|
+
"Time: {timestamp}\n"
|
|
183
|
+
"Value: {value}\n"
|
|
184
|
+
"Confidence interval: {confidence_interval}\n"
|
|
185
|
+
"Detector: {detector_name}\n"
|
|
186
|
+
"Parameters: {detector_params}\n"
|
|
187
|
+
"Direction: {direction}\n"
|
|
188
|
+
"Severity: {severity:.2f}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def __repr__(self) -> str:
|
|
192
|
+
"""String representation of channel."""
|
|
193
|
+
return f"{self.__class__.__name__}()"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Email alert channel implementation.
|
|
3
|
+
|
|
4
|
+
Sends anomaly alerts via SMTP email.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import smtplib
|
|
8
|
+
from email.mime.multipart import MIMEMultipart
|
|
9
|
+
from email.mime.text import MIMEText
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmailChannel(BaseAlertChannel):
|
|
16
|
+
"""
|
|
17
|
+
Email alert channel using SMTP.
|
|
18
|
+
|
|
19
|
+
Sends formatted emails via SMTP server.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
smtp_host: SMTP server hostname
|
|
23
|
+
smtp_port: SMTP server port
|
|
24
|
+
smtp_username: SMTP authentication username
|
|
25
|
+
smtp_password: SMTP authentication password
|
|
26
|
+
from_email: Sender email address
|
|
27
|
+
to_emails: List of recipient email addresses
|
|
28
|
+
use_tls: Whether to use TLS encryption
|
|
29
|
+
subject_template: Email subject template
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> channel = EmailChannel(
|
|
33
|
+
... smtp_host="smtp.gmail.com",
|
|
34
|
+
... smtp_port=587,
|
|
35
|
+
... smtp_username="alerts@example.com",
|
|
36
|
+
... smtp_password="password",
|
|
37
|
+
... from_email="alerts@example.com",
|
|
38
|
+
... to_emails=["team@example.com"]
|
|
39
|
+
... )
|
|
40
|
+
>>> alert = AlertData(
|
|
41
|
+
... metric_name="cpu_usage",
|
|
42
|
+
... timestamp=np.datetime64("2024-01-01T10:00:00"),
|
|
43
|
+
... value=95.0,
|
|
44
|
+
... is_anomaly=True
|
|
45
|
+
... )
|
|
46
|
+
>>> channel.send(alert)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
smtp_host: str,
|
|
52
|
+
smtp_port: int,
|
|
53
|
+
from_email: str,
|
|
54
|
+
to_emails: List[str],
|
|
55
|
+
smtp_username: Optional[str] = None,
|
|
56
|
+
smtp_password: Optional[str] = None,
|
|
57
|
+
use_tls: bool = True,
|
|
58
|
+
subject_template: str = "Anomaly Alert: {metric_name}",
|
|
59
|
+
template: Optional[str] = None,
|
|
60
|
+
**kwargs,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Initialize email channel.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
smtp_host: SMTP server hostname
|
|
67
|
+
smtp_port: SMTP server port (typically 587 for TLS, 465 for SSL)
|
|
68
|
+
from_email: Sender email address
|
|
69
|
+
to_emails: List of recipient email addresses
|
|
70
|
+
smtp_username: SMTP authentication username (optional)
|
|
71
|
+
smtp_password: SMTP authentication password (optional)
|
|
72
|
+
use_tls: Whether to use STARTTLS (default: True)
|
|
73
|
+
subject_template: Email subject template with {metric_name} placeholder
|
|
74
|
+
template: Custom message template (optional)
|
|
75
|
+
**kwargs: Additional parameters (ignored)
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If required parameters are missing
|
|
79
|
+
"""
|
|
80
|
+
if not smtp_host:
|
|
81
|
+
raise ValueError("smtp_host is required for EmailChannel")
|
|
82
|
+
if not smtp_port:
|
|
83
|
+
raise ValueError("smtp_port is required for EmailChannel")
|
|
84
|
+
if not from_email:
|
|
85
|
+
raise ValueError("from_email is required for EmailChannel")
|
|
86
|
+
if not to_emails:
|
|
87
|
+
raise ValueError("to_emails is required for EmailChannel")
|
|
88
|
+
|
|
89
|
+
self.smtp_host = smtp_host
|
|
90
|
+
self.smtp_port = smtp_port
|
|
91
|
+
self.smtp_username = smtp_username
|
|
92
|
+
self.smtp_password = smtp_password
|
|
93
|
+
self.from_email = from_email
|
|
94
|
+
self.to_emails = to_emails
|
|
95
|
+
self.use_tls = use_tls
|
|
96
|
+
self.subject_template = subject_template
|
|
97
|
+
self.template = template
|
|
98
|
+
|
|
99
|
+
def send(self, alert_data: AlertData) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Send alert via email.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
alert_data: Alert information to send
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
smtplib.SMTPException: If email sending fails
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
>>> channel.send(alert_data)
|
|
111
|
+
"""
|
|
112
|
+
message_body = self.format_message(alert_data, self.template)
|
|
113
|
+
|
|
114
|
+
# Create email message
|
|
115
|
+
msg = MIMEMultipart("alternative")
|
|
116
|
+
msg["From"] = self.from_email
|
|
117
|
+
msg["To"] = ", ".join(self.to_emails)
|
|
118
|
+
msg["Subject"] = self.subject_template.format(
|
|
119
|
+
metric_name=alert_data.metric_name
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Attach plain text body
|
|
123
|
+
msg.attach(MIMEText(message_body, "plain"))
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Connect to SMTP server
|
|
127
|
+
if self.use_tls:
|
|
128
|
+
server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10)
|
|
129
|
+
server.starttls()
|
|
130
|
+
else:
|
|
131
|
+
server = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port, timeout=10)
|
|
132
|
+
|
|
133
|
+
# Login if credentials provided
|
|
134
|
+
if self.smtp_username and self.smtp_password:
|
|
135
|
+
server.login(self.smtp_username, self.smtp_password)
|
|
136
|
+
|
|
137
|
+
# Send email
|
|
138
|
+
server.sendmail(self.from_email, self.to_emails, msg.as_string())
|
|
139
|
+
server.quit()
|
|
140
|
+
|
|
141
|
+
except smtplib.SMTPException as e:
|
|
142
|
+
raise smtplib.SMTPException(f"Failed to send email alert: {e}")
|
|
143
|
+
|
|
144
|
+
def __repr__(self) -> str:
|
|
145
|
+
"""String representation."""
|
|
146
|
+
return f"EmailChannel(to={self.to_emails})"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alert channel factory for creating channel instances from configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
from detectkit.alerting.channels.base import BaseAlertChannel
|
|
9
|
+
from detectkit.alerting.channels.mattermost import MattermostChannel
|
|
10
|
+
from detectkit.alerting.channels.slack import SlackChannel
|
|
11
|
+
from detectkit.alerting.channels.webhook import WebhookChannel
|
|
12
|
+
from detectkit.alerting.channels.telegram import TelegramChannel
|
|
13
|
+
from detectkit.alerting.channels.email import EmailChannel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AlertChannelFactory:
|
|
17
|
+
"""
|
|
18
|
+
Factory for creating alert channel instances from configuration.
|
|
19
|
+
|
|
20
|
+
Supports environment variable interpolation in config values.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> factory = AlertChannelFactory()
|
|
24
|
+
>>> channel = factory.create("mattermost", {"webhook_url": "https://..."})
|
|
25
|
+
>>> isinstance(channel, MattermostChannel)
|
|
26
|
+
True
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Registry of available channel types
|
|
30
|
+
CHANNEL_TYPES = {
|
|
31
|
+
"webhook": WebhookChannel,
|
|
32
|
+
"mattermost": MattermostChannel,
|
|
33
|
+
"slack": SlackChannel,
|
|
34
|
+
"telegram": TelegramChannel,
|
|
35
|
+
"email": EmailChannel,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def create(cls, channel_type: str, params: Dict) -> BaseAlertChannel:
|
|
40
|
+
"""
|
|
41
|
+
Create alert channel instance from type and parameters.
|
|
42
|
+
|
|
43
|
+
Supports environment variable interpolation:
|
|
44
|
+
- ${ENV_VAR} or {{ env_var('ENV_VAR') }}
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
channel_type: Type of channel (e.g., "mattermost", "slack")
|
|
48
|
+
params: Channel parameters
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Alert channel instance
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If channel type is unknown
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> channel = AlertChannelFactory.create(
|
|
58
|
+
... "mattermost",
|
|
59
|
+
... {"webhook_url": "${MATTERMOST_WEBHOOK}"}
|
|
60
|
+
... )
|
|
61
|
+
"""
|
|
62
|
+
channel_type = channel_type.lower()
|
|
63
|
+
|
|
64
|
+
if channel_type not in cls.CHANNEL_TYPES:
|
|
65
|
+
available = ", ".join(sorted(cls.CHANNEL_TYPES.keys()))
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Unknown channel type: '{channel_type}'. "
|
|
68
|
+
f"Available types: {available}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Interpolate environment variables in params
|
|
72
|
+
interpolated_params = cls._interpolate_env_vars(params)
|
|
73
|
+
|
|
74
|
+
channel_class = cls.CHANNEL_TYPES[channel_type]
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
return channel_class(**interpolated_params)
|
|
78
|
+
except TypeError as e:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid parameters for {channel_type} channel: {e}"
|
|
81
|
+
) from e
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _interpolate_env_vars(cls, params: Dict) -> Dict:
|
|
85
|
+
"""
|
|
86
|
+
Interpolate environment variables in parameter values.
|
|
87
|
+
|
|
88
|
+
Supports formats:
|
|
89
|
+
- ${VAR_NAME}
|
|
90
|
+
- {{ env_var('VAR_NAME') }}
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
params: Parameters dictionary
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Parameters with interpolated values
|
|
97
|
+
"""
|
|
98
|
+
import re
|
|
99
|
+
|
|
100
|
+
interpolated = {}
|
|
101
|
+
|
|
102
|
+
for key, value in params.items():
|
|
103
|
+
if isinstance(value, str):
|
|
104
|
+
# Handle ${VAR} format
|
|
105
|
+
value = re.sub(
|
|
106
|
+
r'\$\{([^}]+)\}',
|
|
107
|
+
lambda m: os.environ.get(m.group(1), m.group(0)),
|
|
108
|
+
value,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Handle {{ env_var('VAR') }} format
|
|
112
|
+
value = re.sub(
|
|
113
|
+
r"\{\{\s*env_var\(['\"]([^'\"]+)['\"]\)\s*\}\}",
|
|
114
|
+
lambda m: os.environ.get(m.group(1), m.group(0)),
|
|
115
|
+
value,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
interpolated[key] = value
|
|
119
|
+
|
|
120
|
+
return interpolated
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def create_from_config(cls, channel_config: Dict) -> BaseAlertChannel:
|
|
124
|
+
"""
|
|
125
|
+
Create channel from configuration dictionary.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
channel_config: Configuration with 'type' and channel-specific params
|
|
129
|
+
Example: {
|
|
130
|
+
"type": "mattermost",
|
|
131
|
+
"webhook_url": "${MATTERMOST_WEBHOOK}",
|
|
132
|
+
"username": "detectkit"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Alert channel instance
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> config = {
|
|
140
|
+
... "type": "mattermost",
|
|
141
|
+
... "webhook_url": "https://example.com/hooks/xxx"
|
|
142
|
+
... }
|
|
143
|
+
>>> channel = AlertChannelFactory.create_from_config(config)
|
|
144
|
+
"""
|
|
145
|
+
channel_type = channel_config.get("type")
|
|
146
|
+
if not channel_type:
|
|
147
|
+
raise ValueError("Channel config must have 'type' field")
|
|
148
|
+
|
|
149
|
+
# Extract all params except 'type'
|
|
150
|
+
params = {k: v for k, v in channel_config.items() if k != "type"}
|
|
151
|
+
|
|
152
|
+
return cls.create(channel_type, params)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def create_multiple(cls, channel_configs: List[Dict]) -> List[BaseAlertChannel]:
|
|
156
|
+
"""
|
|
157
|
+
Create multiple channels from list of configurations.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
channel_configs: List of channel configurations
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of channel instances
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> configs = [
|
|
167
|
+
... {"type": "mattermost", "webhook_url": "https://..."},
|
|
168
|
+
... {"type": "slack", "webhook_url": "https://...", "channel": "#alerts"},
|
|
169
|
+
... ]
|
|
170
|
+
>>> channels = AlertChannelFactory.create_multiple(configs)
|
|
171
|
+
>>> len(channels)
|
|
172
|
+
2
|
|
173
|
+
"""
|
|
174
|
+
channels = []
|
|
175
|
+
for config in channel_configs:
|
|
176
|
+
channel = cls.create_from_config(config)
|
|
177
|
+
channels.append(channel)
|
|
178
|
+
return channels
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def list_available_types(cls) -> List[str]:
|
|
182
|
+
"""
|
|
183
|
+
Get list of available channel types.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of channel type names
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> types = AlertChannelFactory.list_available_types()
|
|
190
|
+
>>> "mattermost" in types
|
|
191
|
+
True
|
|
192
|
+
"""
|
|
193
|
+
return sorted(cls.CHANNEL_TYPES.keys())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mattermost alert channel.
|
|
3
|
+
|
|
4
|
+
Convenience wrapper around WebhookChannel for Mattermost.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from detectkit.alerting.channels.webhook import WebhookChannel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MattermostChannel(WebhookChannel):
|
|
13
|
+
"""
|
|
14
|
+
Mattermost alert channel using incoming webhooks.
|
|
15
|
+
|
|
16
|
+
This is a convenience wrapper around WebhookChannel specifically
|
|
17
|
+
for Mattermost. Mattermost webhooks are compatible with Slack API,
|
|
18
|
+
so WebhookChannel can be used directly.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
webhook_url (str): Mattermost incoming webhook URL
|
|
22
|
+
username (str): Bot username to display (default: "detectk")
|
|
23
|
+
icon_emoji (str): Bot emoji icon (default: ":warning:")
|
|
24
|
+
timeout (int): Request timeout in seconds (default: 10)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> channel = MattermostChannel(
|
|
28
|
+
... webhook_url="https://mattermost.example.com/hooks/xxx"
|
|
29
|
+
... )
|
|
30
|
+
>>> success = channel.send(alert_data)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
webhook_url: str,
|
|
36
|
+
username: str = "detectk",
|
|
37
|
+
icon_emoji: str = ":warning:",
|
|
38
|
+
channel: Optional[str] = None,
|
|
39
|
+
timeout: int = 10,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize Mattermost channel with webhook URL."""
|
|
42
|
+
super().__init__(
|
|
43
|
+
webhook_url=webhook_url,
|
|
44
|
+
username=username,
|
|
45
|
+
icon_emoji=icon_emoji,
|
|
46
|
+
channel=channel, # Optional: override webhook's default channel
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
"""String representation."""
|
|
52
|
+
url_preview = self.webhook_url[:30] + "..." if len(self.webhook_url) > 30 else self.webhook_url
|
|
53
|
+
return f"MattermostChannel(url='{url_preview}', username='{self.username}')"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slack alert channel.
|
|
3
|
+
|
|
4
|
+
Convenience wrapper around WebhookChannel for Slack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from detectkit.alerting.channels.webhook import WebhookChannel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SlackChannel(WebhookChannel):
|
|
13
|
+
"""
|
|
14
|
+
Slack alert channel using incoming webhooks.
|
|
15
|
+
|
|
16
|
+
This is a convenience wrapper around WebhookChannel specifically
|
|
17
|
+
for Slack. Slack and Mattermost use compatible webhook formats.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
webhook_url (str): Slack incoming webhook URL
|
|
21
|
+
username (str): Bot username to display (default: "detectk")
|
|
22
|
+
icon_emoji (str): Bot emoji icon (default: ":warning:")
|
|
23
|
+
channel (str): Target Slack channel (optional, e.g., "#alerts")
|
|
24
|
+
timeout (int): Request timeout in seconds (default: 10)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> channel = SlackChannel(
|
|
28
|
+
... webhook_url="https://hooks.slack.com/services/xxx",
|
|
29
|
+
... channel="#alerts"
|
|
30
|
+
... )
|
|
31
|
+
>>> success = channel.send(alert_data)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
webhook_url: str,
|
|
37
|
+
username: str = "detectk",
|
|
38
|
+
icon_emoji: str = ":warning:",
|
|
39
|
+
channel: Optional[str] = None,
|
|
40
|
+
timeout: int = 10,
|
|
41
|
+
):
|
|
42
|
+
"""Initialize Slack channel with webhook URL."""
|
|
43
|
+
super().__init__(
|
|
44
|
+
webhook_url=webhook_url,
|
|
45
|
+
username=username,
|
|
46
|
+
icon_emoji=icon_emoji,
|
|
47
|
+
channel=channel,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
"""String representation."""
|
|
53
|
+
url_preview = self.webhook_url[:30] + "..." if len(self.webhook_url) > 30 else self.webhook_url
|
|
54
|
+
channel_info = f", channel='{self.channel}'" if self.channel else ""
|
|
55
|
+
return f"SlackChannel(url='{url_preview}', username='{self.username}'{channel_info})"
|