detectkit 0.3.8__tar.gz → 0.3.10__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.3.10/PKG-INFO +181 -0
- detectkit-0.3.10/README.md +119 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/__init__.py +1 -1
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/orchestrator.py +14 -8
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/cli/commands/test_alert.py +44 -43
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/config/metric_config.py +12 -2
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/database/internal_tables.py +8 -7
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/orchestration/task_manager.py +85 -86
- detectkit-0.3.10/detectkit.egg-info/PKG-INFO +181 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/pyproject.toml +1 -1
- detectkit-0.3.8/PKG-INFO +0 -252
- detectkit-0.3.8/README.md +0 -190
- detectkit-0.3.8/detectkit.egg-info/PKG-INFO +0 -252
- {detectkit-0.3.8 → detectkit-0.3.10}/LICENSE +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/MANIFEST.in +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/cli/main.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/config/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/config/profile.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/config/project_config.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/config/validator.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/core/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/core/interval.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/core/models.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/database/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/database/manager.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/database/tables.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/base.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit/utils/stats.py +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/requirements.txt +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/setup.cfg +0 -0
- {detectkit-0.3.8 → detectkit-0.3.10}/setup.py +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: detectkit
|
|
3
|
+
Version: 0.3.10
|
|
4
|
+
Summary: Metric monitoring with automatic anomaly detection
|
|
5
|
+
Author: detectkit team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexeiveselov92/detectkit
|
|
8
|
+
Project-URL: Documentation, https://github.com/alexeiveselov92/detectkit
|
|
9
|
+
Project-URL: Repository, https://github.com/alexeiveselov92/detectkit
|
|
10
|
+
Project-URL: Issues, https://github.com/alexeiveselov92/detectkit/issues
|
|
11
|
+
Keywords: monitoring,anomaly-detection,metrics,timeseries,alerting
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.24.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: click>=8.0
|
|
29
|
+
Requires-Dist: jinja2>=3.0
|
|
30
|
+
Requires-Dist: orjson>=3.0
|
|
31
|
+
Requires-Dist: requests>=2.25.0
|
|
32
|
+
Provides-Extra: clickhouse
|
|
33
|
+
Requires-Dist: clickhouse-driver>=0.2.0; extra == "clickhouse"
|
|
34
|
+
Provides-Extra: postgres
|
|
35
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
|
|
36
|
+
Provides-Extra: mysql
|
|
37
|
+
Requires-Dist: pymysql>=1.0.0; extra == "mysql"
|
|
38
|
+
Provides-Extra: all-db
|
|
39
|
+
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
|
|
40
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
|
|
41
|
+
Requires-Dist: pymysql>=1.0.0; extra == "all-db"
|
|
42
|
+
Provides-Extra: prophet
|
|
43
|
+
Requires-Dist: prophet>=1.1.0; extra == "prophet"
|
|
44
|
+
Provides-Extra: timesfm
|
|
45
|
+
Requires-Dist: timesfm>=0.1.0; extra == "timesfm"
|
|
46
|
+
Provides-Extra: advanced-detectors
|
|
47
|
+
Requires-Dist: prophet>=1.1.0; extra == "advanced-detectors"
|
|
48
|
+
Requires-Dist: timesfm>=0.1.0; extra == "advanced-detectors"
|
|
49
|
+
Provides-Extra: all
|
|
50
|
+
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
|
|
51
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
52
|
+
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
53
|
+
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
54
|
+
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
57
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
58
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
59
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
60
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
61
|
+
Dynamic: license-file
|
|
62
|
+
|
|
63
|
+
# detectkit
|
|
64
|
+
|
|
65
|
+
[](https://pypi.org/project/detectkit/)
|
|
66
|
+
[](https://pypi.org/project/detectkit/)
|
|
67
|
+
|
|
68
|
+
**Metric monitoring with automatic anomaly detection.**
|
|
69
|
+
|
|
70
|
+
`detectkit` is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **Pure numpy arrays** — no pandas dependency in core logic
|
|
75
|
+
- **Statistical detectors** — Z-Score, MAD, IQR, Manual Bounds
|
|
76
|
+
- **Multi-channel alerting** — Mattermost, Slack, Telegram, Email, Webhook
|
|
77
|
+
- **@mentions** — tag users/groups in alerts, each channel formats natively
|
|
78
|
+
- **Alert lifecycle** — consecutive anomalies, cooldown, recovery notifications
|
|
79
|
+
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL
|
|
80
|
+
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
81
|
+
- **CLI** — `dtk init`, `dtk run --select`, tag-based selectors
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install detectkit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
With database drivers:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install detectkit[clickhouse] # ClickHouse
|
|
93
|
+
pip install detectkit[all-db] # All databases
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quick Start
|
|
97
|
+
|
|
98
|
+
### CLI (Recommended)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Create project
|
|
102
|
+
dtk init my_monitoring
|
|
103
|
+
cd my_monitoring
|
|
104
|
+
|
|
105
|
+
# Configure database in profiles.yml, then:
|
|
106
|
+
dtk run --select cpu_usage
|
|
107
|
+
dtk run --select tag:critical
|
|
108
|
+
dtk run --select cpu_usage --steps load,detect
|
|
109
|
+
dtk run --select cpu_usage --from 2024-01-01
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Metric Configuration
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
# metrics/api_errors.yml
|
|
116
|
+
name: api_error_rate
|
|
117
|
+
interval: "5min"
|
|
118
|
+
|
|
119
|
+
query: |
|
|
120
|
+
SELECT
|
|
121
|
+
toStartOfInterval(timestamp, INTERVAL 5 MINUTE) AS timestamp,
|
|
122
|
+
countIf(status_code >= 500) / count() * 100 AS value
|
|
123
|
+
FROM http_requests
|
|
124
|
+
WHERE timestamp >= %(from_date)s AND timestamp < %(to_date)s
|
|
125
|
+
GROUP BY timestamp ORDER BY timestamp
|
|
126
|
+
|
|
127
|
+
detectors:
|
|
128
|
+
- type: mad
|
|
129
|
+
params:
|
|
130
|
+
threshold: 3.0
|
|
131
|
+
window_size: 2016 # 7 days
|
|
132
|
+
|
|
133
|
+
alerting:
|
|
134
|
+
enabled: true
|
|
135
|
+
channels: [mattermost_ops]
|
|
136
|
+
consecutive_anomalies: 3
|
|
137
|
+
direction: "up"
|
|
138
|
+
mentions: [oncall_engineer, here]
|
|
139
|
+
alert_cooldown: "30min"
|
|
140
|
+
notify_on_recovery: true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Python API
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import numpy as np
|
|
147
|
+
from detectkit.detectors.statistical import ZScoreDetector
|
|
148
|
+
|
|
149
|
+
detector = ZScoreDetector(threshold=3.0, window_size=100)
|
|
150
|
+
results = detector.detect({
|
|
151
|
+
'timestamp': np.array([...], dtype='datetime64[ms]'),
|
|
152
|
+
'value': np.array([1.0, 2.0, 1.5, 10.0, 1.8]),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
for r in results:
|
|
156
|
+
if r.is_anomaly:
|
|
157
|
+
print(f"Anomaly at {r.timestamp}: {r.value}")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
161
|
+
|
|
162
|
+
- [Getting Started](docs/getting-started/quickstart.md) — 5-minute quickstart
|
|
163
|
+
- [Configuration Guide](docs/guides/configuration.md) — all config options
|
|
164
|
+
- [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
|
|
165
|
+
- [Alerting Guide](docs/guides/alerting.md) — channels, mentions, cooldown, recovery
|
|
166
|
+
- [CLI Reference](docs/reference/cli.md) — command-line documentation
|
|
167
|
+
- [Examples](docs/examples/) — real-world monitoring scenarios
|
|
168
|
+
- [Changelog](CHANGELOG.md) — version history
|
|
169
|
+
|
|
170
|
+
## Requirements
|
|
171
|
+
|
|
172
|
+
- Python 3.10+
|
|
173
|
+
- numpy >= 1.24.0
|
|
174
|
+
- pydantic >= 2.0.0
|
|
175
|
+
- click >= 8.0
|
|
176
|
+
- PyYAML >= 6.0
|
|
177
|
+
- Jinja2 >= 3.0
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# detectkit
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/detectkit/)
|
|
4
|
+
[](https://pypi.org/project/detectkit/)
|
|
5
|
+
|
|
6
|
+
**Metric monitoring with automatic anomaly detection.**
|
|
7
|
+
|
|
8
|
+
`detectkit` is a Python library for data analysts and engineers to monitor time-series metrics with automatic anomaly detection and alerting. dbt-like project structure and CLI.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Pure numpy arrays** — no pandas dependency in core logic
|
|
13
|
+
- **Statistical detectors** — Z-Score, MAD, IQR, Manual Bounds
|
|
14
|
+
- **Multi-channel alerting** — Mattermost, Slack, Telegram, Email, Webhook
|
|
15
|
+
- **@mentions** — tag users/groups in alerts, each channel formats natively
|
|
16
|
+
- **Alert lifecycle** — consecutive anomalies, cooldown, recovery notifications
|
|
17
|
+
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL
|
|
18
|
+
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
19
|
+
- **CLI** — `dtk init`, `dtk run --select`, tag-based selectors
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install detectkit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With database drivers:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install detectkit[clickhouse] # ClickHouse
|
|
31
|
+
pip install detectkit[all-db] # All databases
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### CLI (Recommended)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Create project
|
|
40
|
+
dtk init my_monitoring
|
|
41
|
+
cd my_monitoring
|
|
42
|
+
|
|
43
|
+
# Configure database in profiles.yml, then:
|
|
44
|
+
dtk run --select cpu_usage
|
|
45
|
+
dtk run --select tag:critical
|
|
46
|
+
dtk run --select cpu_usage --steps load,detect
|
|
47
|
+
dtk run --select cpu_usage --from 2024-01-01
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Metric Configuration
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# metrics/api_errors.yml
|
|
54
|
+
name: api_error_rate
|
|
55
|
+
interval: "5min"
|
|
56
|
+
|
|
57
|
+
query: |
|
|
58
|
+
SELECT
|
|
59
|
+
toStartOfInterval(timestamp, INTERVAL 5 MINUTE) AS timestamp,
|
|
60
|
+
countIf(status_code >= 500) / count() * 100 AS value
|
|
61
|
+
FROM http_requests
|
|
62
|
+
WHERE timestamp >= %(from_date)s AND timestamp < %(to_date)s
|
|
63
|
+
GROUP BY timestamp ORDER BY timestamp
|
|
64
|
+
|
|
65
|
+
detectors:
|
|
66
|
+
- type: mad
|
|
67
|
+
params:
|
|
68
|
+
threshold: 3.0
|
|
69
|
+
window_size: 2016 # 7 days
|
|
70
|
+
|
|
71
|
+
alerting:
|
|
72
|
+
enabled: true
|
|
73
|
+
channels: [mattermost_ops]
|
|
74
|
+
consecutive_anomalies: 3
|
|
75
|
+
direction: "up"
|
|
76
|
+
mentions: [oncall_engineer, here]
|
|
77
|
+
alert_cooldown: "30min"
|
|
78
|
+
notify_on_recovery: true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Python API
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import numpy as np
|
|
85
|
+
from detectkit.detectors.statistical import ZScoreDetector
|
|
86
|
+
|
|
87
|
+
detector = ZScoreDetector(threshold=3.0, window_size=100)
|
|
88
|
+
results = detector.detect({
|
|
89
|
+
'timestamp': np.array([...], dtype='datetime64[ms]'),
|
|
90
|
+
'value': np.array([1.0, 2.0, 1.5, 10.0, 1.8]),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
for r in results:
|
|
94
|
+
if r.is_anomaly:
|
|
95
|
+
print(f"Anomaly at {r.timestamp}: {r.value}")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
|
|
100
|
+
- [Getting Started](docs/getting-started/quickstart.md) — 5-minute quickstart
|
|
101
|
+
- [Configuration Guide](docs/guides/configuration.md) — all config options
|
|
102
|
+
- [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
|
|
103
|
+
- [Alerting Guide](docs/guides/alerting.md) — channels, mentions, cooldown, recovery
|
|
104
|
+
- [CLI Reference](docs/reference/cli.md) — command-line documentation
|
|
105
|
+
- [Examples](docs/examples/) — real-world monitoring scenarios
|
|
106
|
+
- [Changelog](CHANGELOG.md) — version history
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- Python 3.10+
|
|
111
|
+
- numpy >= 1.24.0
|
|
112
|
+
- pydantic >= 2.0.0
|
|
113
|
+
- click >= 8.0
|
|
114
|
+
- PyYAML >= 6.0
|
|
115
|
+
- Jinja2 >= 3.0
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
@@ -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.3.10"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -519,15 +519,21 @@ class AlertOrchestrator:
|
|
|
519
519
|
)
|
|
520
520
|
detection_records.append(record)
|
|
521
521
|
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
522
|
+
# Group by timestamp and sort (same format as should_alert uses)
|
|
523
|
+
detections_by_time = self._group_by_timestamp(detection_records)
|
|
524
|
+
timestamps_sorted = sorted(detections_by_time.keys(), reverse=True)
|
|
525
|
+
|
|
526
|
+
# Check that latest post-alert point is NOT anomalous
|
|
527
|
+
# (prevents false recovery when there are fewer post-alert points
|
|
528
|
+
# than consecutive_anomalies threshold)
|
|
529
|
+
latest_ts = timestamps_sorted[0]
|
|
530
|
+
latest_detections = detections_by_time[latest_ts]
|
|
531
|
+
latest_anomalies = [d for d in latest_detections if d.is_anomaly]
|
|
532
|
+
if len(latest_anomalies) >= self.conditions.min_detectors:
|
|
533
|
+
# Latest point is still anomalous — no recovery
|
|
534
|
+
return False
|
|
528
535
|
|
|
529
|
-
|
|
530
|
-
return consecutive < self.conditions.consecutive_anomalies
|
|
536
|
+
return True
|
|
531
537
|
|
|
532
538
|
def should_send_recovery(
|
|
533
539
|
self,
|
|
@@ -115,14 +115,14 @@ def run_test_alert(metric_name: str, profile: Optional[str] = None):
|
|
|
115
115
|
return
|
|
116
116
|
|
|
117
117
|
# Check if alerting is configured
|
|
118
|
-
if not metric_config.alerting
|
|
118
|
+
if not metric_config.alerting:
|
|
119
119
|
print(f"Error: Alerting not enabled for metric '{metric_name}'")
|
|
120
120
|
print("Enable alerting in metric config (alerting.enabled: true)")
|
|
121
121
|
return
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
print("
|
|
123
|
+
active_configs = [c for c in metric_config.alerting if c.enabled and c.channels]
|
|
124
|
+
if not active_configs:
|
|
125
|
+
print(f"Error: No active alert configs for metric '{metric_name}'")
|
|
126
126
|
return
|
|
127
127
|
|
|
128
128
|
# Load profiles
|
|
@@ -138,50 +138,51 @@ def run_test_alert(metric_name: str, profile: Optional[str] = None):
|
|
|
138
138
|
|
|
139
139
|
alert_channels_config = profiles_data.get("alert_channels", {})
|
|
140
140
|
|
|
141
|
-
# Get timezone for display
|
|
142
|
-
timezone_display = metric_config.alerting.timezone or "UTC"
|
|
143
|
-
|
|
144
|
-
# Create mock alert data
|
|
145
141
|
print(f"\n📨 Sending test alert for metric: {metric_name}")
|
|
146
|
-
print(f" Timezone: {timezone_display}")
|
|
147
|
-
print(f" Channels: {', '.join(metric_config.alerting.channels)}\n")
|
|
148
142
|
|
|
149
|
-
|
|
143
|
+
total_success = 0
|
|
144
|
+
total_channels = 0
|
|
150
145
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
for i, alerting_config in enumerate(active_configs):
|
|
147
|
+
timezone_display = alerting_config.timezone or "UTC"
|
|
148
|
+
if len(active_configs) > 1:
|
|
149
|
+
print(f"\n [config {i + 1}/{len(active_configs)}]")
|
|
150
|
+
print(f" Timezone: {timezone_display}")
|
|
151
|
+
print(f" Channels: {', '.join(alerting_config.channels)}\n")
|
|
157
152
|
|
|
158
|
-
|
|
153
|
+
alert_data = create_mock_alert_data(metric_config, timezone_display)
|
|
159
154
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
155
|
+
success_count = 0
|
|
156
|
+
for channel_name in alerting_config.channels:
|
|
157
|
+
total_channels += 1
|
|
158
|
+
if channel_name not in alert_channels_config:
|
|
159
|
+
print(f"⚠️ Channel '{channel_name}' not found in profiles.yml - skipping")
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
channel_config = alert_channels_config[channel_name]
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
channel = AlertChannelFactory.create_from_config(channel_config)
|
|
166
|
+
|
|
167
|
+
template = alerting_config.template_consecutive or None
|
|
168
|
+
|
|
169
|
+
print(f" → Sending to {channel_name}...", end=" ")
|
|
170
|
+
success = channel.send(alert_data, template=template)
|
|
171
|
+
|
|
172
|
+
if success:
|
|
173
|
+
print("✓ SUCCESS")
|
|
174
|
+
success_count += 1
|
|
175
|
+
total_success += 1
|
|
176
|
+
else:
|
|
177
|
+
print("✗ FAILED")
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"✗ ERROR: {e}")
|
|
181
|
+
|
|
182
|
+
print(f"\n{'✓' if success_count > 0 else '✗'} Sent test alert to {success_count}/{len(alerting_config.channels)} channels")
|
|
183
|
+
|
|
184
|
+
if len(active_configs) > 1:
|
|
185
|
+
print(f"\nTotal: {total_success}/{total_channels} channels across {len(active_configs)} alert configs")
|
|
185
186
|
|
|
186
187
|
if success_count > 0:
|
|
187
188
|
print("\n💡 Check your configured channels to verify message formatting")
|
|
@@ -341,9 +341,19 @@ class MetricConfig(BaseModel):
|
|
|
341
341
|
detectors: List[DetectorConfig] = Field(
|
|
342
342
|
default_factory=list, description="Detector configurations"
|
|
343
343
|
)
|
|
344
|
-
alerting: Optional[AlertConfig] = Field(
|
|
345
|
-
default=None, description="Alert configuration"
|
|
344
|
+
alerting: Optional[List[AlertConfig]] = Field(
|
|
345
|
+
default=None, description="Alert configuration(s) — single dict or list of dicts"
|
|
346
346
|
)
|
|
347
|
+
|
|
348
|
+
@field_validator("alerting", mode="before")
|
|
349
|
+
@classmethod
|
|
350
|
+
def normalize_alerting(cls, v):
|
|
351
|
+
"""Normalize alerting to list. Accepts single dict/AlertConfig (backward compat) or list."""
|
|
352
|
+
if v is None:
|
|
353
|
+
return None
|
|
354
|
+
if isinstance(v, (dict, AlertConfig)):
|
|
355
|
+
return [v]
|
|
356
|
+
return v
|
|
347
357
|
tables: Optional[TablesConfig] = Field(
|
|
348
358
|
default=None, description="Custom table names (overrides defaults)"
|
|
349
359
|
)
|
|
@@ -814,13 +814,14 @@ class InternalTablesManager:
|
|
|
814
814
|
no_data_alert = 0
|
|
815
815
|
min_detectors = 1
|
|
816
816
|
|
|
817
|
-
if metric_config.alerting
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
817
|
+
first_alerting = metric_config.alerting[0] if metric_config.alerting else None
|
|
818
|
+
if first_alerting:
|
|
819
|
+
is_alert_enabled = 1 if first_alerting.enabled else 0
|
|
820
|
+
timezone_str = first_alerting.timezone
|
|
821
|
+
direction = first_alerting.direction
|
|
822
|
+
consecutive_anomalies = first_alerting.consecutive_anomalies
|
|
823
|
+
no_data_alert = 1 if first_alerting.no_data_alert else 0
|
|
824
|
+
min_detectors = first_alerting.min_detectors
|
|
824
825
|
|
|
825
826
|
# Prepare data for INSERT
|
|
826
827
|
data = {
|