detectkit 0.2.0__tar.gz → 0.2.1__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.2.0/detectkit.egg-info → detectkit-0.2.1}/PKG-INFO +1 -1
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/base.py +2 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/orchestrator.py +4 -3
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/database/internal_tables.py +142 -2
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/database/tables.py +2 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/statistical/iqr.py +2 -2
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/statistical/mad.py +2 -2
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/statistical/zscore.py +2 -2
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/orchestration/task_manager.py +32 -46
- {detectkit-0.2.0 → detectkit-0.2.1/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.2.0 → detectkit-0.2.1}/pyproject.toml +1 -1
- {detectkit-0.2.0 → detectkit-0.2.1}/LICENSE +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/MANIFEST.in +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/README.md +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/cli/main.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/config/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/config/profile.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/config/project_config.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/config/validator.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/core/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/core/interval.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/core/models.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/database/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/database/manager.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/base.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit/utils/stats.py +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/requirements.txt +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/setup.cfg +0 -0
- {detectkit-0.2.0 → detectkit-0.2.1}/setup.py +0 -0
|
@@ -159,6 +159,7 @@ class BaseAlertChannel(ABC):
|
|
|
159
159
|
confidence_upper=alert_data.confidence_upper,
|
|
160
160
|
confidence_interval=confidence_str,
|
|
161
161
|
detector_name=alert_data.detector_name,
|
|
162
|
+
detector_params=alert_data.detector_params,
|
|
162
163
|
direction=alert_data.direction,
|
|
163
164
|
severity=alert_data.severity,
|
|
164
165
|
consecutive_count=alert_data.consecutive_count,
|
|
@@ -182,6 +183,7 @@ class BaseAlertChannel(ABC):
|
|
|
182
183
|
"Value: {value}\n"
|
|
183
184
|
"Confidence interval: {confidence_interval}\n"
|
|
184
185
|
"Detector: {detector_name}\n"
|
|
186
|
+
"Parameters: {detector_params}\n"
|
|
185
187
|
"Direction: {direction}\n"
|
|
186
188
|
"Severity: {severity:.2f}"
|
|
187
189
|
)
|
|
@@ -35,6 +35,7 @@ class DetectionRecord:
|
|
|
35
35
|
timestamp: np.datetime64
|
|
36
36
|
detector_name: str
|
|
37
37
|
detector_id: str
|
|
38
|
+
detector_params: str # JSON string with detector parameters
|
|
38
39
|
value: float
|
|
39
40
|
is_anomaly: bool
|
|
40
41
|
confidence_lower: Optional[float]
|
|
@@ -242,9 +243,9 @@ class AlertOrchestrator:
|
|
|
242
243
|
detector_names = [d.detector_name for d in anomalies]
|
|
243
244
|
detector_name = f"{len(anomalies)} detectors"
|
|
244
245
|
detector_params_list = [
|
|
245
|
-
f"{d.detector_name}
|
|
246
|
+
f"{d.detector_name}: {d.detector_params}" for d in anomalies
|
|
246
247
|
]
|
|
247
|
-
detector_params = "
|
|
248
|
+
detector_params = "; ".join(detector_params_list)
|
|
248
249
|
|
|
249
250
|
# Combine metadata
|
|
250
251
|
combined_metadata = {
|
|
@@ -256,7 +257,7 @@ class AlertOrchestrator:
|
|
|
256
257
|
else:
|
|
257
258
|
max_severity = primary.severity
|
|
258
259
|
detector_name = primary.detector_name
|
|
259
|
-
detector_params =
|
|
260
|
+
detector_params = primary.detector_params
|
|
260
261
|
combined_metadata = primary.detection_metadata
|
|
261
262
|
|
|
262
263
|
# Convert numpy timestamp for AlertData
|
|
@@ -9,7 +9,7 @@ methods underneath. It does NOT duplicate logic - just provides semantic wrapper
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
|
-
from typing import Dict, Optional
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
|
|
@@ -142,6 +142,7 @@ class InternalTablesManager:
|
|
|
142
142
|
self,
|
|
143
143
|
metric_name: str,
|
|
144
144
|
detector_id: str,
|
|
145
|
+
detector_name: str,
|
|
145
146
|
data: Dict[str, np.ndarray],
|
|
146
147
|
detector_params: str,
|
|
147
148
|
) -> int:
|
|
@@ -151,6 +152,7 @@ class InternalTablesManager:
|
|
|
151
152
|
Args:
|
|
152
153
|
metric_name: Metric identifier
|
|
153
154
|
detector_id: Detector identifier (hash)
|
|
155
|
+
detector_name: Detector class name (e.g., "MADDetector")
|
|
154
156
|
data: Dictionary with keys:
|
|
155
157
|
- timestamp: np.array of datetime64
|
|
156
158
|
- is_anomaly: np.array of bool
|
|
@@ -175,7 +177,7 @@ class InternalTablesManager:
|
|
|
175
177
|
... "detection_metadata": np.array(['{"severity": 0.0}', '{"severity": 0.8}']),
|
|
176
178
|
... }
|
|
177
179
|
>>> rows = internal.save_detections(
|
|
178
|
-
... "cpu_usage", "mad_abc123", data, '{"threshold": 3.0}'
|
|
180
|
+
... "cpu_usage", "mad_abc123", "MADDetector", data, '{"threshold": 3.0}'
|
|
179
181
|
... )
|
|
180
182
|
"""
|
|
181
183
|
num_rows = len(data["timestamp"])
|
|
@@ -184,6 +186,7 @@ class InternalTablesManager:
|
|
|
184
186
|
insert_data = {
|
|
185
187
|
"metric_name": np.full(num_rows, metric_name, dtype=object),
|
|
186
188
|
"detector_id": np.full(num_rows, detector_id, dtype=object),
|
|
189
|
+
"detector_name": np.full(num_rows, detector_name, dtype=object),
|
|
187
190
|
"timestamp": data["timestamp"],
|
|
188
191
|
"is_anomaly": data["is_anomaly"],
|
|
189
192
|
"confidence_lower": data["confidence_lower"],
|
|
@@ -417,6 +420,143 @@ class InternalTablesManager:
|
|
|
417
420
|
# ClickHouse ALTER TABLE DELETE is async, return 0
|
|
418
421
|
return 0
|
|
419
422
|
|
|
423
|
+
def get_recent_detections(
|
|
424
|
+
self,
|
|
425
|
+
metric_name: str,
|
|
426
|
+
last_point: datetime,
|
|
427
|
+
num_points: int,
|
|
428
|
+
) -> List[Dict]:
|
|
429
|
+
"""
|
|
430
|
+
Get recent detection results grouped by timestamp.
|
|
431
|
+
|
|
432
|
+
This method is fully database-agnostic - uses simple SELECT
|
|
433
|
+
and groups data in Python (no GROUP BY, no database-specific functions).
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
metric_name: Metric identifier
|
|
437
|
+
last_point: Last complete timestamp to query up to
|
|
438
|
+
num_points: Number of recent timestamps to retrieve
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of dicts, each containing:
|
|
442
|
+
- timestamp: Detection timestamp
|
|
443
|
+
- detector_ids: List of detector IDs for this timestamp
|
|
444
|
+
- detector_names: List of detector names
|
|
445
|
+
- detector_params_list: List of detector params (JSON strings)
|
|
446
|
+
- is_anomaly_flags: List of is_anomaly bools
|
|
447
|
+
- confidence_lowers: List of lower confidence bounds
|
|
448
|
+
- confidence_uppers: List of upper confidence bounds
|
|
449
|
+
- value: Metric value (same for all detectors at this timestamp)
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> detections = internal.get_recent_detections(
|
|
453
|
+
... "cpu_usage",
|
|
454
|
+
... datetime(2024, 1, 1, 12, 0, 0),
|
|
455
|
+
... 5
|
|
456
|
+
... )
|
|
457
|
+
>>> for det in detections:
|
|
458
|
+
... print(f"{det['timestamp']}: {len(det['detector_ids'])} detectors")
|
|
459
|
+
"""
|
|
460
|
+
full_table_name = self._manager.get_full_table_name(
|
|
461
|
+
TABLE_DETECTIONS, use_internal=True
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Step 1: Get distinct timestamps (database-agnostic)
|
|
465
|
+
# Find last N timestamps with detections
|
|
466
|
+
timestamps_query = f"""
|
|
467
|
+
SELECT DISTINCT timestamp
|
|
468
|
+
FROM {full_table_name}
|
|
469
|
+
WHERE metric_name = %(metric_name)s
|
|
470
|
+
AND timestamp <= %(last_point)s
|
|
471
|
+
ORDER BY timestamp DESC
|
|
472
|
+
LIMIT %(num_points)s
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
timestamp_results = self._manager.execute_query(
|
|
476
|
+
timestamps_query,
|
|
477
|
+
params={
|
|
478
|
+
"metric_name": metric_name,
|
|
479
|
+
"last_point": last_point,
|
|
480
|
+
"num_points": num_points,
|
|
481
|
+
},
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if not timestamp_results:
|
|
485
|
+
return []
|
|
486
|
+
|
|
487
|
+
# Extract timestamps
|
|
488
|
+
timestamps = [row["timestamp"] for row in timestamp_results]
|
|
489
|
+
|
|
490
|
+
# Step 2: Get all detections for these timestamps (simple SELECT)
|
|
491
|
+
# Build IN clause with timestamps
|
|
492
|
+
timestamps_str = ", ".join([
|
|
493
|
+
f"'{ts.strftime('%Y-%m-%d %H:%M:%S')}'" for ts in timestamps
|
|
494
|
+
])
|
|
495
|
+
|
|
496
|
+
detections_query = f"""
|
|
497
|
+
SELECT
|
|
498
|
+
timestamp,
|
|
499
|
+
detector_id,
|
|
500
|
+
detector_name,
|
|
501
|
+
detector_params,
|
|
502
|
+
is_anomaly,
|
|
503
|
+
confidence_lower,
|
|
504
|
+
confidence_upper,
|
|
505
|
+
value
|
|
506
|
+
FROM {full_table_name}
|
|
507
|
+
WHERE metric_name = %(metric_name)s
|
|
508
|
+
AND timestamp IN ({timestamps_str})
|
|
509
|
+
ORDER BY timestamp DESC, detector_id
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
detection_results = self._manager.execute_query(
|
|
513
|
+
detections_query,
|
|
514
|
+
params={"metric_name": metric_name},
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if not detection_results:
|
|
518
|
+
return []
|
|
519
|
+
|
|
520
|
+
# Step 3: Group by timestamp in Python (no pandas, pure Python)
|
|
521
|
+
# Use timestamp string as key to avoid datetime comparison issues
|
|
522
|
+
grouped = {}
|
|
523
|
+
for row in detection_results:
|
|
524
|
+
ts = row["timestamp"]
|
|
525
|
+
# Convert timestamp to string key for grouping
|
|
526
|
+
if isinstance(ts, str):
|
|
527
|
+
ts_key = ts
|
|
528
|
+
ts_value = ts
|
|
529
|
+
else:
|
|
530
|
+
# datetime object - normalize and convert to string
|
|
531
|
+
if hasattr(ts, 'tzinfo') and ts.tzinfo is not None:
|
|
532
|
+
ts = ts.replace(tzinfo=None)
|
|
533
|
+
ts_key = ts.isoformat()
|
|
534
|
+
ts_value = ts
|
|
535
|
+
|
|
536
|
+
if ts_key not in grouped:
|
|
537
|
+
grouped[ts_key] = {
|
|
538
|
+
"timestamp": ts_value,
|
|
539
|
+
"detector_ids": [],
|
|
540
|
+
"detector_names": [],
|
|
541
|
+
"detector_params_list": [],
|
|
542
|
+
"is_anomaly_flags": [],
|
|
543
|
+
"confidence_lowers": [],
|
|
544
|
+
"confidence_uppers": [],
|
|
545
|
+
"value": row["value"], # Same for all detectors at this timestamp
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
grouped[ts_key]["detector_ids"].append(row["detector_id"])
|
|
549
|
+
grouped[ts_key]["detector_names"].append(row["detector_name"])
|
|
550
|
+
grouped[ts_key]["detector_params_list"].append(row["detector_params"])
|
|
551
|
+
grouped[ts_key]["is_anomaly_flags"].append(row["is_anomaly"])
|
|
552
|
+
grouped[ts_key]["confidence_lowers"].append(row["confidence_lower"])
|
|
553
|
+
grouped[ts_key]["confidence_uppers"].append(row["confidence_upper"])
|
|
554
|
+
|
|
555
|
+
# Step 4: Convert to list, sorted by timestamp key (desc)
|
|
556
|
+
result = [grouped[ts_key] for ts_key in sorted(grouped.keys(), reverse=True)]
|
|
557
|
+
|
|
558
|
+
return result
|
|
559
|
+
|
|
420
560
|
def acquire_lock(
|
|
421
561
|
self,
|
|
422
562
|
metric_name: str,
|
|
@@ -48,6 +48,7 @@ def get_detections_table_model() -> TableModel:
|
|
|
48
48
|
Schema:
|
|
49
49
|
- metric_name: Metric identifier
|
|
50
50
|
- detector_id: Detector identifier (hash of class + params)
|
|
51
|
+
- detector_name: Detector class name (e.g., "MADDetector", "ZScoreDetector")
|
|
51
52
|
- timestamp: Detection timestamp (UTC, millisecond precision)
|
|
52
53
|
- is_anomaly: Whether point is anomalous
|
|
53
54
|
- confidence_lower: Lower confidence bound
|
|
@@ -64,6 +65,7 @@ def get_detections_table_model() -> TableModel:
|
|
|
64
65
|
columns=[
|
|
65
66
|
ColumnDefinition("metric_name", "String"),
|
|
66
67
|
ColumnDefinition("detector_id", "String"),
|
|
68
|
+
ColumnDefinition("detector_name", "String"),
|
|
67
69
|
ColumnDefinition("timestamp", "DateTime64(3, 'UTC')"),
|
|
68
70
|
ColumnDefinition("is_anomaly", "Bool"),
|
|
69
71
|
ColumnDefinition("confidence_lower", "Nullable(Float64)", nullable=True),
|
|
@@ -348,8 +348,8 @@ class IQRDetector(BaseDetector):
|
|
|
348
348
|
)
|
|
349
349
|
|
|
350
350
|
# Apply mask to window (only valid values + seasonality match)
|
|
351
|
-
|
|
352
|
-
combined_mask
|
|
351
|
+
# Both valid_mask and season_mask are same size as window_processed
|
|
352
|
+
combined_mask = valid_mask & season_mask
|
|
353
353
|
|
|
354
354
|
group_values = window_processed[combined_mask]
|
|
355
355
|
|
|
@@ -336,8 +336,8 @@ class MADDetector(BaseDetector):
|
|
|
336
336
|
)
|
|
337
337
|
|
|
338
338
|
# Apply mask to window (only valid values + seasonality match)
|
|
339
|
-
|
|
340
|
-
combined_mask
|
|
339
|
+
# Both valid_mask and season_mask are same size as window_processed
|
|
340
|
+
combined_mask = valid_mask & season_mask
|
|
341
341
|
|
|
342
342
|
group_values = window_processed[combined_mask]
|
|
343
343
|
|
|
@@ -342,8 +342,8 @@ class ZScoreDetector(BaseDetector):
|
|
|
342
342
|
)
|
|
343
343
|
|
|
344
344
|
# Apply mask to window (only valid values + seasonality match)
|
|
345
|
-
|
|
346
|
-
combined_mask
|
|
345
|
+
# Both valid_mask and season_mask are same size as window_processed
|
|
346
|
+
combined_mask = valid_mask & season_mask
|
|
347
347
|
|
|
348
348
|
group_values = window_processed[combined_mask]
|
|
349
349
|
|
|
@@ -356,6 +356,9 @@ class TaskManager:
|
|
|
356
356
|
metric_name=config.name,
|
|
357
357
|
detector_id=detector_id
|
|
358
358
|
)
|
|
359
|
+
# Normalize last_detection_ts to naive if needed
|
|
360
|
+
if last_detection_ts and last_detection_ts.tzinfo is not None:
|
|
361
|
+
last_detection_ts = last_detection_ts.replace(tzinfo=None)
|
|
359
362
|
|
|
360
363
|
# Determine actual from_date
|
|
361
364
|
actual_from = normalized_from_date
|
|
@@ -374,13 +377,14 @@ class TaskManager:
|
|
|
374
377
|
# Always normalize to naive datetime
|
|
375
378
|
start_time = start_time.replace(tzinfo=None)
|
|
376
379
|
if actual_from:
|
|
377
|
-
# Ensure actual_from is also naive
|
|
378
|
-
if actual_from.tzinfo is not None:
|
|
379
|
-
actual_from = actual_from.replace(tzinfo=None)
|
|
380
380
|
actual_from = max(actual_from, start_time)
|
|
381
381
|
else:
|
|
382
382
|
actual_from = start_time
|
|
383
383
|
|
|
384
|
+
# Ensure actual_from is naive (for comparison with actual_to)
|
|
385
|
+
if actual_from and actual_from.tzinfo is not None:
|
|
386
|
+
actual_from = actual_from.replace(tzinfo=None)
|
|
387
|
+
|
|
384
388
|
# Skip if nothing to detect
|
|
385
389
|
if not actual_from or actual_from >= actual_to:
|
|
386
390
|
continue
|
|
@@ -451,6 +455,7 @@ class TaskManager:
|
|
|
451
455
|
self.internal.save_detections(
|
|
452
456
|
metric_name=config.name,
|
|
453
457
|
detector_id=detector_id,
|
|
458
|
+
detector_name=detector.__class__.__name__,
|
|
454
459
|
data=detection_data,
|
|
455
460
|
detector_params=detector.get_detector_params(),
|
|
456
461
|
)
|
|
@@ -543,37 +548,11 @@ class TaskManager:
|
|
|
543
548
|
Returns:
|
|
544
549
|
List of DetectionRecord objects
|
|
545
550
|
"""
|
|
546
|
-
#
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
# Query _dtk_detections for recent points
|
|
553
|
-
# GROUP BY timestamp to combine results from multiple detectors
|
|
554
|
-
query = f"""
|
|
555
|
-
SELECT
|
|
556
|
-
timestamp,
|
|
557
|
-
groupArray(detector_id) as detector_ids,
|
|
558
|
-
groupArray(is_anomaly) as is_anomaly_flags,
|
|
559
|
-
groupArray(confidence_lower) as confidence_lowers,
|
|
560
|
-
groupArray(confidence_upper) as confidence_uppers,
|
|
561
|
-
any(value) as value
|
|
562
|
-
FROM {full_table_name}
|
|
563
|
-
WHERE metric_name = %(metric_name)s
|
|
564
|
-
AND timestamp <= %(last_point)s
|
|
565
|
-
GROUP BY timestamp
|
|
566
|
-
ORDER BY timestamp DESC
|
|
567
|
-
LIMIT %(num_points)s
|
|
568
|
-
"""
|
|
569
|
-
|
|
570
|
-
results = self.db_manager.execute_query(
|
|
571
|
-
query,
|
|
572
|
-
params={
|
|
573
|
-
"metric_name": metric_name,
|
|
574
|
-
"last_point": last_point,
|
|
575
|
-
"num_points": num_points,
|
|
576
|
-
},
|
|
551
|
+
# Use internal tables manager method (database-agnostic)
|
|
552
|
+
results = self.internal.get_recent_detections(
|
|
553
|
+
metric_name=metric_name,
|
|
554
|
+
last_point=last_point,
|
|
555
|
+
num_points=num_points,
|
|
577
556
|
)
|
|
578
557
|
|
|
579
558
|
if not results:
|
|
@@ -585,10 +564,10 @@ class TaskManager:
|
|
|
585
564
|
# Check if any detector flagged this point as anomaly
|
|
586
565
|
is_anomaly = any(row["is_anomaly_flags"])
|
|
587
566
|
|
|
588
|
-
# Get detector
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
for
|
|
567
|
+
# Get detector data for anomalous detections
|
|
568
|
+
anomaly_indices = [
|
|
569
|
+
i
|
|
570
|
+
for i, flag in enumerate(row["is_anomaly_flags"])
|
|
592
571
|
if flag
|
|
593
572
|
]
|
|
594
573
|
|
|
@@ -597,12 +576,18 @@ class TaskManager:
|
|
|
597
576
|
severity = 0.0
|
|
598
577
|
confidence_lower = None
|
|
599
578
|
confidence_upper = None
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
579
|
+
detector_name = "unknown"
|
|
580
|
+
detector_id = "unknown"
|
|
581
|
+
detector_params = "{}"
|
|
582
|
+
|
|
583
|
+
if is_anomaly and anomaly_indices:
|
|
584
|
+
# Get data from first anomalous detector
|
|
585
|
+
first_idx = anomaly_indices[0]
|
|
586
|
+
detector_name = row["detector_names"][first_idx]
|
|
587
|
+
detector_id = row["detector_ids"][first_idx]
|
|
588
|
+
detector_params = row["detector_params_list"][first_idx]
|
|
589
|
+
confidence_lower = row["confidence_lowers"][first_idx]
|
|
590
|
+
confidence_upper = row["confidence_uppers"][first_idx]
|
|
606
591
|
|
|
607
592
|
# Determine direction
|
|
608
593
|
value = row["value"]
|
|
@@ -616,8 +601,9 @@ class TaskManager:
|
|
|
616
601
|
records.append(
|
|
617
602
|
DetectionRecord(
|
|
618
603
|
timestamp=row["timestamp"],
|
|
619
|
-
detector_name=
|
|
620
|
-
detector_id=
|
|
604
|
+
detector_name=detector_name,
|
|
605
|
+
detector_id=detector_id,
|
|
606
|
+
detector_params=detector_params,
|
|
621
607
|
value=row["value"],
|
|
622
608
|
is_anomaly=is_anomaly,
|
|
623
609
|
confidence_lower=confidence_lower,
|
|
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
|