detectkit 0.3.9__tar.gz → 0.3.11__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.9/detectkit.egg-info → detectkit-0.3.11}/PKG-INFO +1 -1
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/__init__.py +1 -1
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/orchestrator.py +14 -8
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/clickhouse_manager.py +40 -4
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/internal_tables.py +3 -1
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/orchestration/task_manager.py +7 -14
- {detectkit-0.3.9 → detectkit-0.3.11/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.3.9 → detectkit-0.3.11}/pyproject.toml +1 -1
- {detectkit-0.3.9 → detectkit-0.3.11}/LICENSE +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/MANIFEST.in +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/README.md +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/main.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/profile.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/project_config.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/validator.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/core/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/core/interval.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/core/models.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/manager.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/tables.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/base.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/utils/stats.py +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/requirements.txt +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/setup.cfg +0 -0
- {detectkit-0.3.9 → detectkit-0.3.11}/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.3.
|
|
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,
|
|
@@ -343,15 +343,48 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
|
|
|
343
343
|
"""
|
|
344
344
|
from detectkit.database.tables import TABLE_TASKS
|
|
345
345
|
|
|
346
|
+
full_table = self.get_full_table_name(TABLE_TASKS, use_internal=True)
|
|
347
|
+
|
|
346
348
|
# Get current UTC time (convert to naive UTC for numpy compatibility)
|
|
347
349
|
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
|
348
350
|
|
|
349
|
-
#
|
|
351
|
+
# Read existing alert tracking fields before delete (preserve across upsert)
|
|
352
|
+
existing_last_alert_sent = None
|
|
353
|
+
existing_last_recovery_sent = None
|
|
354
|
+
existing_alert_count = 0
|
|
355
|
+
|
|
356
|
+
preserve_query = f"""
|
|
357
|
+
SELECT last_alert_sent, last_recovery_sent, alert_count
|
|
358
|
+
FROM {full_table}
|
|
359
|
+
WHERE metric_name = %(metric_name)s
|
|
360
|
+
AND detector_id = %(detector_id)s
|
|
361
|
+
AND process_type = %(process_type)s
|
|
362
|
+
ORDER BY updated_at DESC
|
|
363
|
+
LIMIT 1
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
preserve_results = self.execute_query(
|
|
367
|
+
preserve_query,
|
|
368
|
+
params={
|
|
369
|
+
"metric_name": metric_name,
|
|
370
|
+
"detector_id": detector_id,
|
|
371
|
+
"process_type": process_type,
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
if preserve_results:
|
|
375
|
+
existing_last_alert_sent = preserve_results[0].get("last_alert_sent")
|
|
376
|
+
existing_last_recovery_sent = preserve_results[0].get("last_recovery_sent")
|
|
377
|
+
existing_alert_count = preserve_results[0].get("alert_count", 0) or 0
|
|
378
|
+
except Exception:
|
|
379
|
+
pass # If read fails, proceed with defaults
|
|
380
|
+
|
|
381
|
+
# Delete existing record (if any), sync to ensure old row is gone before insert
|
|
350
382
|
delete_query = f"""
|
|
351
|
-
ALTER TABLE {
|
|
383
|
+
ALTER TABLE {full_table}
|
|
352
384
|
DELETE WHERE metric_name = %(metric_name)s
|
|
353
385
|
AND detector_id = %(detector_id)s
|
|
354
386
|
AND process_type = %(process_type)s
|
|
387
|
+
SETTINGS mutations_sync = 1
|
|
355
388
|
"""
|
|
356
389
|
|
|
357
390
|
self._client.execute(
|
|
@@ -371,7 +404,7 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
|
|
|
371
404
|
else:
|
|
372
405
|
last_ts_naive = last_processed_timestamp
|
|
373
406
|
|
|
374
|
-
# Then insert new record
|
|
407
|
+
# Then insert new record (preserving alert tracking fields)
|
|
375
408
|
insert_data = {
|
|
376
409
|
"metric_name": np.array([metric_name]),
|
|
377
410
|
"detector_id": np.array([detector_id]),
|
|
@@ -382,10 +415,13 @@ class ClickHouseDatabaseManager(BaseDatabaseManager):
|
|
|
382
415
|
"last_processed_timestamp": np.array([last_ts_naive], dtype="datetime64[ms]") if last_ts_naive else np.array([None]),
|
|
383
416
|
"error_message": np.array([error_message]),
|
|
384
417
|
"timeout_seconds": np.array([timeout_seconds], dtype=np.int32),
|
|
418
|
+
"last_alert_sent": np.array([existing_last_alert_sent], dtype="datetime64[ms]") if existing_last_alert_sent else np.array([None]),
|
|
419
|
+
"alert_count": np.array([existing_alert_count], dtype=np.uint32),
|
|
420
|
+
"last_recovery_sent": np.array([existing_last_recovery_sent], dtype="datetime64[ms]") if existing_last_recovery_sent else np.array([None]),
|
|
385
421
|
}
|
|
386
422
|
|
|
387
423
|
self.insert_batch(
|
|
388
|
-
|
|
424
|
+
full_table,
|
|
389
425
|
insert_data,
|
|
390
426
|
conflict_strategy="ignore"
|
|
391
427
|
)
|
|
@@ -954,6 +954,7 @@ class InternalTablesManager:
|
|
|
954
954
|
WHERE metric_name = %(metric_name)s
|
|
955
955
|
AND detector_id = 'pipeline'
|
|
956
956
|
AND process_type = 'pipeline'
|
|
957
|
+
SETTINGS mutations_sync = 1
|
|
957
958
|
"""
|
|
958
959
|
else:
|
|
959
960
|
# Update without alert_count increment
|
|
@@ -965,6 +966,7 @@ class InternalTablesManager:
|
|
|
965
966
|
WHERE metric_name = %(metric_name)s
|
|
966
967
|
AND detector_id = 'pipeline'
|
|
967
968
|
AND process_type = 'pipeline'
|
|
969
|
+
SETTINGS mutations_sync = 1
|
|
968
970
|
"""
|
|
969
971
|
|
|
970
972
|
self._manager.execute_query(
|
|
@@ -975,7 +977,6 @@ class InternalTablesManager:
|
|
|
975
977
|
}
|
|
976
978
|
)
|
|
977
979
|
|
|
978
|
-
# ClickHouse ALTER TABLE UPDATE is async, return 1 (optimistic)
|
|
979
980
|
return 1
|
|
980
981
|
|
|
981
982
|
def get_last_recovery_timestamp(
|
|
@@ -1049,6 +1050,7 @@ class InternalTablesManager:
|
|
|
1049
1050
|
WHERE metric_name = %(metric_name)s
|
|
1050
1051
|
AND detector_id = 'pipeline'
|
|
1051
1052
|
AND process_type = 'pipeline'
|
|
1053
|
+
SETTINGS mutations_sync = 1
|
|
1052
1054
|
"""
|
|
1053
1055
|
|
|
1054
1056
|
self._manager.execute_query(
|
|
@@ -189,20 +189,13 @@ class TaskManager:
|
|
|
189
189
|
result["anomalies_detected"] = detect_result["anomalies_count"]
|
|
190
190
|
result["steps_completed"].append(PipelineStep.DETECT)
|
|
191
191
|
|
|
192
|
-
# Step 3: Send alerts
|
|
192
|
+
# Step 3: Send alerts (also handles recovery notifications)
|
|
193
193
|
if PipelineStep.ALERT in steps:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
result["alerts_sent"] = 0
|
|
200
|
-
else:
|
|
201
|
-
click.echo()
|
|
202
|
-
click.echo(click.style(" ┌─ ALERT", fg="cyan", bold=True))
|
|
203
|
-
alert_result = self._run_alert_step(config)
|
|
204
|
-
result["alerts_sent"] = alert_result["alerts_sent"]
|
|
205
|
-
result["steps_completed"].append(PipelineStep.ALERT)
|
|
194
|
+
click.echo()
|
|
195
|
+
click.echo(click.style(" ┌─ ALERT", fg="cyan", bold=True))
|
|
196
|
+
alert_result = self._run_alert_step(config)
|
|
197
|
+
result["alerts_sent"] = alert_result["alerts_sent"]
|
|
198
|
+
result["steps_completed"].append(PipelineStep.ALERT)
|
|
206
199
|
|
|
207
200
|
finally:
|
|
208
201
|
# Always release lock
|
|
@@ -600,7 +593,7 @@ class TaskManager:
|
|
|
600
593
|
metric_name=config.name,
|
|
601
594
|
interval=interval,
|
|
602
595
|
conditions=AlertConditions(
|
|
603
|
-
min_detectors=
|
|
596
|
+
min_detectors=alerting_config.min_detectors,
|
|
604
597
|
direction=alerting_config.direction,
|
|
605
598
|
consecutive_anomalies=alerting_config.consecutive_anomalies,
|
|
606
599
|
),
|
|
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
|