Skip to content

Commit 2cfc0a0

Browse files
feat(phase_4): implement core engine orchestrator, prediction logic, stability analysis, and system state model
1 parent 7d0fa64 commit 2cfc0a0

6 files changed

Lines changed: 418 additions & 8 deletions

File tree

dashboard/lib/models/system_state.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ class SystemState {
33
final EngineInfo engine;
44
final StressResult? stress;
55
final NormalizedData? normalized;
6+
final PredictionResult? prediction;
7+
final StabilityIndex? stability;
68
final AlertInfo alert;
79
final BufferInfo buffers;
810

911
SystemState({
1012
required this.engine,
1113
this.stress,
1214
this.normalized,
15+
this.prediction,
16+
this.stability,
1317
required this.alert,
1418
required this.buffers,
1519
});
@@ -23,6 +27,12 @@ class SystemState {
2327
normalized: json['normalized'] != null
2428
? NormalizedData.fromJson(json['normalized'])
2529
: null,
30+
prediction: json['prediction'] != null
31+
? PredictionResult.fromJson(json['prediction'])
32+
: null,
33+
stability: json['stability'] != null
34+
? StabilityIndex.fromJson(json['stability'])
35+
: null,
2636
alert: AlertInfo.fromJson(json['alert'] ?? {}),
2737
buffers: BufferInfo.fromJson(json['buffers'] ?? {}),
2838
);
@@ -309,3 +319,43 @@ class SystemEvent {
309319
);
310320
}
311321
}
322+
323+
class PredictionResult {
324+
final double? memoryExhaustionEtaSec;
325+
final double? cpuCriticalEtaSec;
326+
final double riskScore;
327+
328+
PredictionResult({
329+
this.memoryExhaustionEtaSec,
330+
this.cpuCriticalEtaSec,
331+
required this.riskScore,
332+
});
333+
334+
factory PredictionResult.fromJson(Map<String, dynamic> json) {
335+
return PredictionResult(
336+
memoryExhaustionEtaSec: json['memory_exhaustion_eta_sec']?.toDouble(),
337+
cpuCriticalEtaSec: json['cpu_critical_eta_sec']?.toDouble(),
338+
riskScore: (json['risk_score'] ?? 0).toDouble(),
339+
);
340+
}
341+
}
342+
343+
class StabilityIndex {
344+
final double score;
345+
final String state;
346+
final Map<String, dynamic> components;
347+
348+
StabilityIndex({
349+
required this.score,
350+
required this.state,
351+
required this.components,
352+
});
353+
354+
factory StabilityIndex.fromJson(Map<String, dynamic> json) {
355+
return StabilityIndex(
356+
score: (json['score'] ?? 100).toDouble(),
357+
state: json['state'] ?? 'unknown',
358+
components: Map<String, dynamic>.from(json['components'] ?? {}),
359+
);
360+
}
361+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""
2+
SentraCore — Prediction & Risk Engine.
3+
4+
Forecasts resource exhaustion and calculates degradation probability based
5+
on smoothed historical trends.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
from dataclasses import dataclass
12+
from typing import TYPE_CHECKING
13+
14+
if TYPE_CHECKING:
15+
from engine.intelligence.trend_analyzer import TrendResult
16+
from engine.normalization.normalizer import NormalizedSnapshot
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
@dataclass(frozen=True, slots=True)
22+
class PredictionResult:
23+
"""Forecast of system degradation and resource exhaustion."""
24+
25+
memory_exhaustion_eta_sec: float | None
26+
cpu_critical_eta_sec: float | None
27+
risk_score: float # 0 to 100%
28+
29+
def to_dict(self) -> dict:
30+
return {
31+
"memory_exhaustion_eta_sec": (
32+
round(self.memory_exhaustion_eta_sec, 1) if self.memory_exhaustion_eta_sec else None
33+
),
34+
"cpu_critical_eta_sec": (
35+
round(self.cpu_critical_eta_sec, 1) if self.cpu_critical_eta_sec else None
36+
),
37+
"risk_score": round(self.risk_score, 2),
38+
}
39+
40+
41+
class PredictionEngine:
42+
"""
43+
Computes Time-To-Exhaustion (TTE) for critical resources using
44+
smoothed trend slopes, and produces an overarching risk score.
45+
"""
46+
47+
def __init__(self, smoothing_factor: float = 0.2) -> None:
48+
"""
49+
Args:
50+
smoothing_factor: Weight given to the new slope (EMA) to prevent chaotic ETAs.
51+
"""
52+
self._smoothing_factor = smoothing_factor
53+
self._smoothed_cpu_slope = 0.0
54+
self._smoothed_memory_slope = 0.0
55+
56+
def predict(self, trend: 'TrendResult', snapshot: 'NormalizedSnapshot') -> PredictionResult:
57+
"""
58+
Forecast resource exhaustion.
59+
60+
Args:
61+
trend: The current trend analysis (containing slopes).
62+
snapshot: Current normalized telemetry (for starting points).
63+
64+
Returns:
65+
A PredictionResult indicating ETAs and overall risk.
66+
"""
67+
# Apply Exponential Moving Average (EMA) to the slopes to smooth them
68+
if self._smoothed_cpu_slope == 0.0:
69+
self._smoothed_cpu_slope = trend.cpu_slope
70+
else:
71+
self._smoothed_cpu_slope = (
72+
self._smoothing_factor * trend.cpu_slope
73+
+ (1 - self._smoothing_factor) * self._smoothed_cpu_slope
74+
)
75+
76+
if self._smoothed_memory_slope == 0.0:
77+
self._smoothed_memory_slope = trend.memory_slope
78+
else:
79+
self._smoothed_memory_slope = (
80+
self._smoothing_factor * trend.memory_slope
81+
+ (1 - self._smoothing_factor) * self._smoothed_memory_slope
82+
)
83+
84+
# Calculate Memory ETA to 98%
85+
mem_eta = None
86+
if self._smoothed_memory_slope > 0.05: # Require at least 0.05% growth per second to forecast
87+
remaining_mem = 98.0 - snapshot.memory_percent_smoothed
88+
if remaining_mem > 0:
89+
mem_eta = remaining_mem / self._smoothed_memory_slope
90+
91+
# Calculate CPU ETA to 95%
92+
cpu_eta = None
93+
if self._smoothed_cpu_slope > 0.1: # Require at least 0.1% growth per second to forecast
94+
remaining_cpu = 95.0 - snapshot.cpu_percent_smoothed
95+
if remaining_cpu > 0:
96+
cpu_eta = remaining_cpu / self._smoothed_cpu_slope
97+
98+
# Calculate Probabilistic Risk Score (0-100)
99+
risk_score = 0.0
100+
101+
# Risk from Memory exhaustion proximity
102+
if mem_eta is not None:
103+
if mem_eta < 60:
104+
risk_score += 80.0 # Under 1 minute to OOM is critical
105+
elif mem_eta < 300:
106+
risk_score += 40.0 # Under 5 minutes is high risk
107+
else:
108+
risk_score += 10.0 # Leaking, but slow
109+
110+
# Risk from CPU exhaustion proximity
111+
if cpu_eta is not None:
112+
if cpu_eta < 30:
113+
risk_score += 60.0 # Fast spike to saturation
114+
elif cpu_eta < 120:
115+
risk_score += 30.0
116+
117+
# Base risk based on sheer resource usage, regardless of slope
118+
if snapshot.memory_percent_smoothed > 90.0:
119+
risk_score += 40.0
120+
if snapshot.cpu_percent_smoothed > 90.0:
121+
risk_score += 30.0
122+
123+
return PredictionResult(
124+
memory_exhaustion_eta_sec=mem_eta,
125+
cpu_critical_eta_sec=cpu_eta,
126+
risk_score=min(100.0, risk_score),
127+
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
SentraCore — System Stability Index Calculator.
3+
4+
Computes a global, unified metric of system health (1-100) by blending
5+
instantaneous stress, statistical anomalies, and predictive risk.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
from dataclasses import dataclass
12+
from typing import TYPE_CHECKING
13+
14+
if TYPE_CHECKING:
15+
from engine.intelligence.anomaly_detector import AnomalyResult
16+
from engine.intelligence.prediction_engine import PredictionResult
17+
from engine.stress.stress_engine import StressResult
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
@dataclass(frozen=True, slots=True)
23+
class StabilityIndex:
24+
"""Unified system health score."""
25+
26+
score: float # 1-100 (100 = perfect, 1 = critical)
27+
state: str # "stable", "degraded", "critical"
28+
components: dict # Breakdown of the penalty points
29+
30+
def to_dict(self) -> dict:
31+
return {
32+
"score": round(self.score, 1),
33+
"state": self.state,
34+
"components": self.components,
35+
}
36+
37+
38+
class StabilityCalculator:
39+
"""
40+
Computes System Stability Index.
41+
42+
100 means the system is perfectly stable.
43+
Lower scores indicate increasing levels of degradation.
44+
"""
45+
46+
def __init__(
47+
self,
48+
stress_weight: float = 0.5,
49+
risk_weight: float = 0.3,
50+
anomaly_weight: float = 0.2,
51+
) -> None:
52+
self._w_stress = stress_weight
53+
self._w_risk = risk_weight
54+
self._w_anomaly = anomaly_weight
55+
56+
# Ensure weights sum to 1.0
57+
total = self._w_stress + self._w_risk + self._w_anomaly
58+
self._w_stress /= total
59+
self._w_risk /= total
60+
self._w_anomaly /= total
61+
62+
def calculate(
63+
self,
64+
stress: 'StressResult',
65+
prediction: 'PredictionResult',
66+
anomaly: 'AnomalyResult',
67+
) -> StabilityIndex:
68+
"""
69+
Calculate global stability score.
70+
"""
71+
# Calculate penalty points from each component (0-100)
72+
stress_penalty = stress.score * self._w_stress
73+
risk_penalty = prediction.risk_score * self._w_risk
74+
anomaly_penalty = anomaly.score * self._w_anomaly
75+
76+
total_penalty = stress_penalty + risk_penalty + anomaly_penalty
77+
78+
# Stability is the inverse of the penalty
79+
score = max(1.0, 100.0 - total_penalty)
80+
81+
# Determine state
82+
if score >= 80.0:
83+
state = "stable"
84+
elif score >= 40.0:
85+
state = "degraded"
86+
else:
87+
state = "critical"
88+
89+
return StabilityIndex(
90+
score=score,
91+
state=state,
92+
components={
93+
"stress_penalty": round(stress_penalty, 1),
94+
"risk_penalty": round(risk_penalty, 1),
95+
"anomaly_penalty": round(anomaly_penalty, 1),
96+
},
97+
)

engine/main.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
from engine.stress.stress_engine import StressEngine, StressResult
4040
from engine.intelligence.trend_analyzer import TrendAnalyzer, TrendResult
4141
from engine.intelligence.anomaly_detector import AnomalyDetector, AnomalyResult
42+
from engine.intelligence.prediction_engine import PredictionEngine, PredictionResult
43+
from engine.intelligence.stability_index import StabilityCalculator, StabilityIndex
4244

4345
logger = logging.getLogger(__name__)
4446

@@ -60,14 +62,18 @@ def __init__(self) -> None:
6062
self.event_logger = EventLogger()
6163
self.trend_analyzer = TrendAnalyzer()
6264
self.anomaly_detector = AnomalyDetector()
65+
self.prediction_engine = PredictionEngine()
6366
self.stress_engine = StressEngine()
67+
self.stability_calculator = StabilityCalculator()
6468
self.alert_manager = AlertManager()
6569

6670
# Current state (updated each cycle)
6771
self._current_stress: StressResult | None = None
6872
self._current_normalized: NormalizedSnapshot | None = None
6973
self._current_trend: TrendResult | None = None
7074
self._current_anomaly: AnomalyResult | None = None
75+
self._current_prediction: PredictionResult | None = None
76+
self._current_stability: StabilityIndex | None = None
7177
self._last_alert: Alert | None = None
7278

7379
# Control
@@ -89,7 +95,9 @@ def get_current_state(self) -> dict:
8995
"normalized": self._current_normalized.to_dict() if self._current_normalized else None,
9096
"trend": self._current_trend.to_dict() if self._current_trend else None,
9197
"anomaly": self._current_anomaly.to_dict() if self._current_anomaly else None,
98+
"prediction": self._current_prediction.to_dict() if self._current_prediction else None,
9299
"stress": self._current_stress.to_dict() if self._current_stress else None,
100+
"stability": self._current_stability.to_dict() if self._current_stability else None,
93101
"alert": {
94102
"total_fired": self.alert_manager.total_alerts,
95103
"in_cooldown": self.alert_manager.is_in_cooldown,
@@ -201,7 +209,14 @@ async def run(self) -> None:
201209
stress = self.stress_engine.compute(normalized, trend=trend, anomaly=anomaly)
202210
self._current_stress = stress
203211

204-
# 9. Evaluate alerts
212+
# 9. Phase 4: Prediction & Risk
213+
prediction = self.prediction_engine.predict(trend, normalized)
214+
self._current_prediction = prediction
215+
216+
stability = self.stability_calculator.calculate(stress, prediction, anomaly)
217+
self._current_stability = stability
218+
219+
# 10. Evaluate alerts
205220
top_procs = self.process_tracker.get_top_consumers(5)
206221
recent_events = self.event_logger.get_recent_events(20)
207222
self.alert_manager.evaluate(stress, top_procs, recent_events)
@@ -212,17 +227,15 @@ async def run(self) -> None:
212227
# Periodic log
213228
if cycle % 15 == 0: # Every ~30 seconds
214229
logger.info(
215-
"Cycle %d | Stress: %.0f (%s) | CPU: %.1f%% | Mem: %.1f%% | "
216-
"Disk: %.0f ops/s | Baseline: %s (%d samples) | Events: %d",
230+
"Cycle %d | Stability: %.0f/100 (%s) | Risk: %.0f%% | CPU: %.1f%% | Mem: %.1f%% | "
231+
"Baseline: %s",
217232
cycle,
218-
stress.score,
219-
stress.level,
233+
stability.score,
234+
stability.state,
235+
prediction.risk_score,
220236
normalized.cpu_percent_smoothed,
221237
normalized.memory_percent_smoothed,
222-
normalized.disk_total_ops_per_sec,
223238
"ready" if self.baseline.is_ready else "learning",
224-
self.baseline.sample_count,
225-
self.event_logger.event_count,
226239
)
227240

228241
except Exception as exc:

0 commit comments

Comments
 (0)