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.
Files changed (59) hide show
  1. {detectkit-0.3.9/detectkit.egg-info → detectkit-0.3.11}/PKG-INFO +1 -1
  2. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/__init__.py +1 -1
  3. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/orchestrator.py +14 -8
  4. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/clickhouse_manager.py +40 -4
  5. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/internal_tables.py +3 -1
  6. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/orchestration/task_manager.py +7 -14
  7. {detectkit-0.3.9 → detectkit-0.3.11/detectkit.egg-info}/PKG-INFO +1 -1
  8. {detectkit-0.3.9 → detectkit-0.3.11}/pyproject.toml +1 -1
  9. {detectkit-0.3.9 → detectkit-0.3.11}/LICENSE +0 -0
  10. {detectkit-0.3.9 → detectkit-0.3.11}/MANIFEST.in +0 -0
  11. {detectkit-0.3.9 → detectkit-0.3.11}/README.md +0 -0
  12. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/__init__.py +0 -0
  13. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/__init__.py +0 -0
  14. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/base.py +0 -0
  15. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/email.py +0 -0
  16. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/factory.py +0 -0
  17. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/mattermost.py +0 -0
  18. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/slack.py +0 -0
  19. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/telegram.py +0 -0
  20. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/alerting/channels/webhook.py +0 -0
  21. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/__init__.py +0 -0
  22. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/__init__.py +0 -0
  23. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/init.py +0 -0
  24. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/run.py +0 -0
  25. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/commands/test_alert.py +0 -0
  26. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/cli/main.py +0 -0
  27. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/__init__.py +0 -0
  28. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/metric_config.py +0 -0
  29. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/profile.py +0 -0
  30. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/project_config.py +0 -0
  31. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/config/validator.py +0 -0
  32. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/core/__init__.py +0 -0
  33. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/core/interval.py +0 -0
  34. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/core/models.py +0 -0
  35. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/__init__.py +0 -0
  36. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/manager.py +0 -0
  37. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/database/tables.py +0 -0
  38. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/__init__.py +0 -0
  39. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/base.py +0 -0
  40. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/factory.py +0 -0
  41. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/__init__.py +0 -0
  42. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/iqr.py +0 -0
  43. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/mad.py +0 -0
  44. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  45. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/detectors/statistical/zscore.py +0 -0
  46. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/loaders/__init__.py +0 -0
  47. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/loaders/metric_loader.py +0 -0
  48. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/loaders/query_template.py +0 -0
  49. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/orchestration/__init__.py +0 -0
  50. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/utils/__init__.py +0 -0
  51. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit/utils/stats.py +0 -0
  52. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/SOURCES.txt +0 -0
  53. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/dependency_links.txt +0 -0
  54. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/entry_points.txt +0 -0
  55. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/requires.txt +0 -0
  56. {detectkit-0.3.9 → detectkit-0.3.11}/detectkit.egg-info/top_level.txt +0 -0
  57. {detectkit-0.3.9 → detectkit-0.3.11}/requirements.txt +0 -0
  58. {detectkit-0.3.9 → detectkit-0.3.11}/setup.cfg +0 -0
  59. {detectkit-0.3.9 → detectkit-0.3.11}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.3.9
3
+ Version: 0.3.11
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.3.9"
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
- # Count consecutive anomalies (same logic as should_alert)
523
- consecutive = self._count_consecutive_anomalies(
524
- detections=detection_records,
525
- min_detectors=self.conditions.min_detectors,
526
- direction=self.conditions.direction
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
- # Recovery = consecutive dropped below threshold
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
- # First, delete existing record (if any)
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 {self.get_full_table_name(TABLE_TASKS, use_internal=True)}
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
- self.get_full_table_name(TABLE_TASKS, use_internal=True),
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
- # Skip alert if no anomalies detected in current run
195
- if result.get("anomalies_detected", 0) == 0:
196
- click.echo()
197
- click.echo(click.style(" ┌─ ALERT", fg="cyan", bold=True))
198
- click.echo(" │ No anomalies detected in current run, skipping alerts")
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=1, # At least one detector must flag anomaly
596
+ min_detectors=alerting_config.min_detectors,
604
597
  direction=alerting_config.direction,
605
598
  consecutive_anomalies=alerting_config.consecutive_anomalies,
606
599
  ),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "detectkit"
7
- version = "0.3.9"
7
+ version = "0.3.11"
8
8
  description = "Metric monitoring with automatic anomaly detection"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes