foresight-cli 0.1.0__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.
- foresight_cli-0.1.0/PKG-INFO +27 -0
- foresight_cli-0.1.0/README.md +0 -0
- foresight_cli-0.1.0/foresight/__init__.py +0 -0
- foresight_cli-0.1.0/foresight/cli.py +478 -0
- foresight_cli-0.1.0/foresight/collector.py +50 -0
- foresight_cli-0.1.0/foresight/forecaster.py +245 -0
- foresight_cli-0.1.0/foresight/storage.py +137 -0
- foresight_cli-0.1.0/foresight_cli.egg-info/PKG-INFO +27 -0
- foresight_cli-0.1.0/foresight_cli.egg-info/SOURCES.txt +13 -0
- foresight_cli-0.1.0/foresight_cli.egg-info/dependency_links.txt +1 -0
- foresight_cli-0.1.0/foresight_cli.egg-info/entry_points.txt +2 -0
- foresight_cli-0.1.0/foresight_cli.egg-info/requires.txt +6 -0
- foresight_cli-0.1.0/foresight_cli.egg-info/top_level.txt +1 -0
- foresight_cli-0.1.0/pyproject.toml +51 -0
- foresight_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foresight-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Predict system resource exhaustion before it happens.
|
|
5
|
+
Author: Rishi Garg
|
|
6
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/foresight
|
|
7
|
+
Project-URL: Repository, https://github.com/YOUR_USERNAME/foresight
|
|
8
|
+
Project-URL: Issues, https://github.com/YOUR_USERNAME/foresight/issues
|
|
9
|
+
Keywords: cli,monitoring,forecasting,devops,time-series,arima,machine-learning,psutil,system-monitoring
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Intended Audience :: System Administrators
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: psutil>=5.9.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Requires-Dist: typer>=0.9.0
|
|
25
|
+
Requires-Dist: plotext>=5.2.0
|
|
26
|
+
Requires-Dist: pandas>=2.0.0
|
|
27
|
+
Requires-Dist: statsmodels>=0.14.0
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich import box
|
|
7
|
+
import plotext as plt
|
|
8
|
+
|
|
9
|
+
from foresight.storage import init_db, get_snapshots, get_metric_series, VALID_METRICS
|
|
10
|
+
from foresight.storage import save_snapshot
|
|
11
|
+
from foresight.collector import collect_loop, collect_snapshot
|
|
12
|
+
from foresight.forecaster import (
|
|
13
|
+
forecast_arima,
|
|
14
|
+
forecast_holtwinters,
|
|
15
|
+
forecast_ensemble,
|
|
16
|
+
check_threshold,
|
|
17
|
+
steps_to_human_time,
|
|
18
|
+
parse_horizon,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ─── App Setup ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
name="foresight",
|
|
26
|
+
help="Predict system resource exhaustion before it happens.",
|
|
27
|
+
add_completion=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
# Smart thresholds based on real computer health standards
|
|
33
|
+
METRIC_THRESHOLDS = {
|
|
34
|
+
"cpu_percent": {"warning": 75.0, "critical": 90.0},
|
|
35
|
+
"ram_percent": {"warning": 80.0, "critical": 90.0},
|
|
36
|
+
"disk_percent": {"warning": 85.0, "critical": 95.0},
|
|
37
|
+
"ram_used_mb": {"warning": 80.0, "critical": 90.0},
|
|
38
|
+
"disk_used_gb": {"warning": 85.0, "critical": 95.0},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
VALID_MODELS = ("arima", "holtwinters", "ensemble")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ─── Private Helpers ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def _color(value: float, warning: float = 75.0, critical: float = 90.0) -> str:
|
|
47
|
+
"""Return Rich color string based on value vs thresholds."""
|
|
48
|
+
if value >= critical:
|
|
49
|
+
return "bold red"
|
|
50
|
+
elif value >= warning:
|
|
51
|
+
return "yellow"
|
|
52
|
+
else:
|
|
53
|
+
return "green"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_forecast(metric: str, steps: int, model: str) -> dict:
|
|
57
|
+
"""Run the selected forecast model and return result dict."""
|
|
58
|
+
if model == "arima":
|
|
59
|
+
return forecast_arima(metric=metric, steps=steps)
|
|
60
|
+
elif model == "holtwinters":
|
|
61
|
+
return forecast_holtwinters(metric=metric, steps=steps)
|
|
62
|
+
else:
|
|
63
|
+
return forecast_ensemble(metric=metric, steps=steps)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _validate_inputs(metric: str, model: str) -> bool:
|
|
67
|
+
"""Validate metric and model. Print error and return False if invalid."""
|
|
68
|
+
if metric not in VALID_METRICS:
|
|
69
|
+
console.print(f"[red]Invalid metric '{metric}'.[/red]")
|
|
70
|
+
console.print(f"Valid metrics: {', '.join(sorted(VALID_METRICS))}")
|
|
71
|
+
return False
|
|
72
|
+
if model not in VALID_MODELS:
|
|
73
|
+
console.print(
|
|
74
|
+
f"[red]Invalid model '{model}'. "
|
|
75
|
+
f"Choose: {', '.join(VALID_MODELS)}[/red]"
|
|
76
|
+
)
|
|
77
|
+
return False
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _status_icon(status: str) -> str:
|
|
82
|
+
return {"critical": "🚨", "warning": "⚠️ ", "ok": "✅"}.get(status, "")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _status_style(status: str) -> str:
|
|
86
|
+
return {
|
|
87
|
+
"critical": "bold red",
|
|
88
|
+
"warning": "bold yellow",
|
|
89
|
+
"ok": "bold green",
|
|
90
|
+
}.get(status, "white")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ─── Commands ─────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
@app.command()
|
|
96
|
+
def collect(
|
|
97
|
+
interval: int = typer.Option(
|
|
98
|
+
60, help="Seconds between snapshots (default: 60s)."
|
|
99
|
+
),
|
|
100
|
+
rounds: int = typer.Option(
|
|
101
|
+
60, help="Number of snapshots (default: 60 = 1 hour of data)."
|
|
102
|
+
),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Collect system metrics every N seconds for M rounds."""
|
|
105
|
+
init_db()
|
|
106
|
+
total_minutes = rounds * interval // 60
|
|
107
|
+
console.print(
|
|
108
|
+
f"\n[bold green]Starting collection:[/bold green] "
|
|
109
|
+
f"{rounds} snapshots every {interval}s "
|
|
110
|
+
f"([cyan]~{total_minutes} minutes total[/cyan])\n"
|
|
111
|
+
)
|
|
112
|
+
collect_loop(interval_seconds=interval, rounds=rounds)
|
|
113
|
+
console.print(
|
|
114
|
+
f"\n[bold green]Done.[/bold green] "
|
|
115
|
+
f"{rounds} snapshots saved to database.\n"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def status() -> None:
|
|
121
|
+
"""Show a single live snapshot of current resource usage."""
|
|
122
|
+
init_db()
|
|
123
|
+
snap = collect_snapshot()
|
|
124
|
+
save_snapshot(snap)
|
|
125
|
+
|
|
126
|
+
cpu = snap["cpu_percent"]
|
|
127
|
+
ram = snap["ram_percent"]
|
|
128
|
+
disk = snap["disk_percent"]
|
|
129
|
+
|
|
130
|
+
cpu_thresh = METRIC_THRESHOLDS["cpu_percent"]
|
|
131
|
+
ram_thresh = METRIC_THRESHOLDS["ram_percent"]
|
|
132
|
+
disk_thresh = METRIC_THRESHOLDS["disk_percent"]
|
|
133
|
+
|
|
134
|
+
console.print("\n[bold]Current System Status[/bold]\n")
|
|
135
|
+
console.print(
|
|
136
|
+
f" CPU : [{_color(cpu, **cpu_thresh)}]{cpu:>5.1f}%[/{_color(cpu, **cpu_thresh)}]"
|
|
137
|
+
)
|
|
138
|
+
console.print(
|
|
139
|
+
f" RAM : [{_color(ram, **ram_thresh)}]{ram:>5.1f}%[/{_color(ram, **ram_thresh)}]"
|
|
140
|
+
f" ({snap['ram_used_mb']:.0f} MB / {snap['ram_total_mb']:.0f} MB)"
|
|
141
|
+
)
|
|
142
|
+
console.print(
|
|
143
|
+
f" Disk : [{_color(disk, **disk_thresh)}]{disk:>5.1f}%[/{_color(disk, **disk_thresh)}]"
|
|
144
|
+
f" ({snap['disk_used_gb']:.1f} GB / {snap['disk_total_gb']:.1f} GB)"
|
|
145
|
+
)
|
|
146
|
+
console.print(f"\n [dim]Saved at {snap['timestamp']}[/dim]\n")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command()
|
|
150
|
+
def watch(
|
|
151
|
+
interval: int = typer.Option(5, help="Refresh every N seconds."),
|
|
152
|
+
rounds: int = typer.Option(50, help="Stop after N refreshes."),
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Live monitor — refreshes resource usage every N seconds. Ctrl+C to stop."""
|
|
155
|
+
init_db()
|
|
156
|
+
console.print(
|
|
157
|
+
f"\n[bold]Foresight Live Monitor[/bold] — "
|
|
158
|
+
f"refreshing every {interval}s. "
|
|
159
|
+
f"[dim]Ctrl+C to stop.[/dim]\n"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
for i in range(rounds):
|
|
164
|
+
snap = collect_snapshot()
|
|
165
|
+
save_snapshot(snap)
|
|
166
|
+
|
|
167
|
+
os.system("cls")
|
|
168
|
+
|
|
169
|
+
cpu = snap["cpu_percent"]
|
|
170
|
+
ram = snap["ram_percent"]
|
|
171
|
+
disk = snap["disk_percent"]
|
|
172
|
+
|
|
173
|
+
cpu_thresh = METRIC_THRESHOLDS["cpu_percent"]
|
|
174
|
+
ram_thresh = METRIC_THRESHOLDS["ram_percent"]
|
|
175
|
+
disk_thresh = METRIC_THRESHOLDS["disk_percent"]
|
|
176
|
+
|
|
177
|
+
console.print(
|
|
178
|
+
f"[bold]Foresight Live Monitor[/bold] "
|
|
179
|
+
f"[dim]{snap['timestamp']}[/dim]\n"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
cpu_bar = "█" * int(cpu / 5)
|
|
183
|
+
ram_bar = "█" * int(ram / 5)
|
|
184
|
+
disk_bar = "█" * int(disk / 5)
|
|
185
|
+
|
|
186
|
+
console.print(
|
|
187
|
+
f" CPU : [{_color(cpu, **cpu_thresh)}]{cpu:>5.1f}%[/{_color(cpu, **cpu_thresh)}] {cpu_bar}"
|
|
188
|
+
)
|
|
189
|
+
console.print(
|
|
190
|
+
f" RAM : [{_color(ram, **ram_thresh)}]{ram:>5.1f}%[/{_color(ram, **ram_thresh)}] {ram_bar}"
|
|
191
|
+
f" ({snap['ram_used_mb']:.0f} MB / {snap['ram_total_mb']:.0f} MB)"
|
|
192
|
+
)
|
|
193
|
+
console.print(
|
|
194
|
+
f" Disk : [{_color(disk, **disk_thresh)}]{disk:>5.1f}%[/{_color(disk, **disk_thresh)}] {disk_bar}"
|
|
195
|
+
f" ({snap['disk_used_gb']:.1f} GB / {snap['disk_total_gb']:.1f} GB)"
|
|
196
|
+
)
|
|
197
|
+
console.print(
|
|
198
|
+
f"\n [dim]Refresh {i+1}/{rounds} — next in {interval}s[/dim]"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if i < rounds - 1:
|
|
202
|
+
time.sleep(interval)
|
|
203
|
+
|
|
204
|
+
except KeyboardInterrupt:
|
|
205
|
+
console.print("\n[yellow]Live monitor stopped.[/yellow]\n")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command()
|
|
209
|
+
def show(
|
|
210
|
+
limit: int = typer.Option(10, help="Number of recent snapshots to display."),
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Show recent snapshots from the database as a color-coded table."""
|
|
213
|
+
init_db()
|
|
214
|
+
snapshots = get_snapshots(limit=limit)
|
|
215
|
+
|
|
216
|
+
if not snapshots:
|
|
217
|
+
console.print(
|
|
218
|
+
"[yellow]No snapshots found. "
|
|
219
|
+
"Run 'foresight collect' first.[/yellow]"
|
|
220
|
+
)
|
|
221
|
+
raise typer.Exit()
|
|
222
|
+
|
|
223
|
+
table = Table(
|
|
224
|
+
title=f"Last {len(snapshots)} Snapshots",
|
|
225
|
+
box=box.ROUNDED,
|
|
226
|
+
show_lines=True,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
table.add_column("Timestamp", style="cyan", no_wrap=True)
|
|
230
|
+
table.add_column("CPU %", justify="right")
|
|
231
|
+
table.add_column("RAM %", justify="right")
|
|
232
|
+
table.add_column("RAM Used (MB)", justify="right")
|
|
233
|
+
table.add_column("Disk %", justify="right")
|
|
234
|
+
table.add_column("Disk Used (GB)", justify="right")
|
|
235
|
+
|
|
236
|
+
cpu_t = METRIC_THRESHOLDS["cpu_percent"]
|
|
237
|
+
ram_t = METRIC_THRESHOLDS["ram_percent"]
|
|
238
|
+
disk_t = METRIC_THRESHOLDS["disk_percent"]
|
|
239
|
+
|
|
240
|
+
for snap in reversed(snapshots):
|
|
241
|
+
cpu = snap["cpu_percent"]
|
|
242
|
+
ram = snap["ram_percent"]
|
|
243
|
+
disk = snap["disk_percent"]
|
|
244
|
+
|
|
245
|
+
table.add_row(
|
|
246
|
+
snap["timestamp"],
|
|
247
|
+
f"[{_color(cpu, **cpu_t)}]{cpu}%[/{_color(cpu, **cpu_t)}]",
|
|
248
|
+
f"[{_color(ram, **ram_t)}]{ram}%[/{_color(ram, **ram_t)}]",
|
|
249
|
+
str(snap["ram_used_mb"]),
|
|
250
|
+
f"[{_color(disk, **disk_t)}]{disk}%[/{_color(disk, **disk_t)}]",
|
|
251
|
+
str(snap["disk_used_gb"]),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
console.print(table)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@app.command()
|
|
258
|
+
def chart(
|
|
259
|
+
metric: str = typer.Option("cpu_percent", help="Metric to chart."),
|
|
260
|
+
limit: int = typer.Option(50, help="Number of recent snapshots to plot."),
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Render an ASCII line chart of a metric over time in the terminal."""
|
|
263
|
+
init_db()
|
|
264
|
+
|
|
265
|
+
if metric not in VALID_METRICS:
|
|
266
|
+
console.print(f"[red]Invalid metric '{metric}'.[/red]")
|
|
267
|
+
console.print(f"Valid options: {', '.join(sorted(VALID_METRICS))}")
|
|
268
|
+
raise typer.Exit()
|
|
269
|
+
|
|
270
|
+
timestamps, values = get_metric_series(metric=metric, limit=limit)
|
|
271
|
+
|
|
272
|
+
if len(values) < 2:
|
|
273
|
+
console.print(
|
|
274
|
+
"[yellow]Not enough data to chart. "
|
|
275
|
+
"Run 'foresight collect' first.[/yellow]"
|
|
276
|
+
)
|
|
277
|
+
raise typer.Exit()
|
|
278
|
+
|
|
279
|
+
short_labels = [ts[11:16] for ts in timestamps]
|
|
280
|
+
x_values = list(range(len(values)))
|
|
281
|
+
|
|
282
|
+
plt.clear_figure()
|
|
283
|
+
plt.plot(x_values, values, label=metric, marker="braille")
|
|
284
|
+
plt.title(
|
|
285
|
+
f"{metric} — {short_labels[0]} to {short_labels[-1]} "
|
|
286
|
+
f"({len(values)} snapshots)"
|
|
287
|
+
)
|
|
288
|
+
plt.xlabel("Snapshot index")
|
|
289
|
+
plt.ylabel("Percent (%)")
|
|
290
|
+
plt.ylim(0, 100)
|
|
291
|
+
plt.plotsize(80, 20)
|
|
292
|
+
plt.show()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@app.command()
|
|
296
|
+
def forecast(
|
|
297
|
+
metric: str = typer.Option("cpu_percent", help="Metric to forecast."),
|
|
298
|
+
horizon: str = typer.Option("30m", help="How far ahead: '30m', '1h', '2h'."),
|
|
299
|
+
model: str = typer.Option("ensemble", help="Model: arima, holtwinters, ensemble."),
|
|
300
|
+
interval: int = typer.Option(60, help="Your collection interval in seconds."),
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Forecast future resource usage over a time horizon (e.g. 30m, 1h, 2h)."""
|
|
303
|
+
init_db()
|
|
304
|
+
|
|
305
|
+
if not _validate_inputs(metric, model):
|
|
306
|
+
raise typer.Exit()
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
steps = parse_horizon(horizon, interval_seconds=interval)
|
|
310
|
+
except ValueError as e:
|
|
311
|
+
console.print(f"[red]{e}[/red]")
|
|
312
|
+
raise typer.Exit()
|
|
313
|
+
|
|
314
|
+
console.print(
|
|
315
|
+
f"\n[bold]Forecasting[/bold] [cyan]{metric}[/cyan] "
|
|
316
|
+
f"for next [cyan]{horizon}[/cyan] "
|
|
317
|
+
f"({steps} steps × {interval}s) "
|
|
318
|
+
f"using [cyan]{model.upper()}[/cyan]\n"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
result = _run_forecast(metric, steps, model)
|
|
322
|
+
|
|
323
|
+
trend_color = {"rising": "red", "falling": "green", "stable": "yellow"}.get(
|
|
324
|
+
result["trend_summary"], "white"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
console.print(f" Last observed : {result['last_observed']}%")
|
|
328
|
+
console.print(
|
|
329
|
+
f" Trend : "
|
|
330
|
+
f"[{trend_color}]{result['trend_summary']}[/{trend_color}]"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if "components" in result:
|
|
334
|
+
console.print(
|
|
335
|
+
f"\n [dim]ARIMA : {result['components']['arima']}[/dim]"
|
|
336
|
+
)
|
|
337
|
+
console.print(
|
|
338
|
+
f" [dim]Holt-Winters : {result['components']['holtwinters']}[/dim]"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
thresh = METRIC_THRESHOLDS.get(metric, {"warning": 75.0, "critical": 90.0})
|
|
342
|
+
|
|
343
|
+
console.print(f"\n {'Time':>10} {'Value':>8} Chart")
|
|
344
|
+
console.print(f" {'─'*10} {'─'*8} {'─'*20}")
|
|
345
|
+
|
|
346
|
+
for i, val in enumerate(result["forecast"], start=1):
|
|
347
|
+
bar = "█" * int(val / 5)
|
|
348
|
+
time_label = steps_to_human_time(i, interval_seconds=interval)
|
|
349
|
+
col = _color(val, **thresh)
|
|
350
|
+
console.print(
|
|
351
|
+
f" {time_label:>10} : [{col}]{val:>6.2f}%[/{col}] {bar}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
console.print()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.command()
|
|
358
|
+
def alert(
|
|
359
|
+
metric: str = typer.Option("cpu_percent", help="Metric to check."),
|
|
360
|
+
horizon: str = typer.Option("30m", help="How far ahead: '30m', '1h', '2h'."),
|
|
361
|
+
threshold: float = typer.Option(-1.0, help="Custom threshold %. Uses smart default if omitted."),
|
|
362
|
+
model: str = typer.Option("ensemble", help="Model: arima, holtwinters, ensemble."),
|
|
363
|
+
interval: int = typer.Option(60, help="Your collection interval in seconds."),
|
|
364
|
+
) -> None:
|
|
365
|
+
"""Check if a metric is predicted to breach its health threshold."""
|
|
366
|
+
init_db()
|
|
367
|
+
|
|
368
|
+
if not _validate_inputs(metric, model):
|
|
369
|
+
raise typer.Exit()
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
steps = parse_horizon(horizon, interval_seconds=interval)
|
|
373
|
+
except ValueError as e:
|
|
374
|
+
console.print(f"[red]{e}[/red]")
|
|
375
|
+
raise typer.Exit()
|
|
376
|
+
|
|
377
|
+
# Apply smart default threshold if user didn't provide one
|
|
378
|
+
if threshold == -1.0:
|
|
379
|
+
threshold = METRIC_THRESHOLDS.get(
|
|
380
|
+
metric, {"warning": 85.0}
|
|
381
|
+
)["warning"]
|
|
382
|
+
console.print(
|
|
383
|
+
f"\n [dim]Using smart default threshold: "
|
|
384
|
+
f"{threshold}% for {metric}[/dim]"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
result = _run_forecast(metric, steps, model)
|
|
388
|
+
alert_result = check_threshold(forecast_result=result, threshold=threshold)
|
|
389
|
+
|
|
390
|
+
status = alert_result["status"]
|
|
391
|
+
icon = _status_icon(status)
|
|
392
|
+
style = _status_style(status)
|
|
393
|
+
|
|
394
|
+
console.print(
|
|
395
|
+
f"\n {icon} [{style}]{status.upper()}[/{style}]"
|
|
396
|
+
f" — {alert_result['message']}\n"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if alert_result["breaches"]:
|
|
400
|
+
console.print(f" [dim]Predicted breaches above {threshold}%:[/dim]")
|
|
401
|
+
for b in alert_result["breaches"]:
|
|
402
|
+
time_label = steps_to_human_time(b["step"], interval_seconds=interval)
|
|
403
|
+
console.print(
|
|
404
|
+
f" {time_label:>10} : [red]{b['value']}%[/red]"
|
|
405
|
+
)
|
|
406
|
+
console.print()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@app.command()
|
|
410
|
+
def healthcheck(
|
|
411
|
+
horizon: str = typer.Option("30m", help="How far ahead to check: '30m', '1h', '2h'."),
|
|
412
|
+
model: str = typer.Option("ensemble", help="Model: arima, holtwinters, ensemble."),
|
|
413
|
+
interval: int = typer.Option(60, help="Your collection interval in seconds."),
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Run a full health check across CPU, RAM, and Disk at once."""
|
|
416
|
+
init_db()
|
|
417
|
+
|
|
418
|
+
if model not in VALID_MODELS:
|
|
419
|
+
console.print(f"[red]Invalid model. Choose: {', '.join(VALID_MODELS)}[/red]")
|
|
420
|
+
raise typer.Exit()
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
steps = parse_horizon(horizon, interval_seconds=interval)
|
|
424
|
+
except ValueError as e:
|
|
425
|
+
console.print(f"[red]{e}[/red]")
|
|
426
|
+
raise typer.Exit()
|
|
427
|
+
|
|
428
|
+
metrics_to_check = [
|
|
429
|
+
("cpu_percent", METRIC_THRESHOLDS["cpu_percent"]["warning"]),
|
|
430
|
+
("ram_percent", METRIC_THRESHOLDS["ram_percent"]["warning"]),
|
|
431
|
+
("disk_percent", METRIC_THRESHOLDS["disk_percent"]["warning"]),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
console.print(
|
|
435
|
+
f"\n[bold]System Health Check[/bold] — "
|
|
436
|
+
f"next [cyan]{horizon}[/cyan] "
|
|
437
|
+
f"via [cyan]{model.upper()}[/cyan]\n"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
overall_status = "ok"
|
|
441
|
+
|
|
442
|
+
for metric, threshold in metrics_to_check:
|
|
443
|
+
result = _run_forecast(metric, steps, model)
|
|
444
|
+
alert_result = check_threshold(forecast_result=result, threshold=threshold)
|
|
445
|
+
status = alert_result["status"]
|
|
446
|
+
|
|
447
|
+
# Escalate overall only upward: ok → warning → critical
|
|
448
|
+
if status == "critical":
|
|
449
|
+
overall_status = "critical"
|
|
450
|
+
elif status == "warning" and overall_status == "ok":
|
|
451
|
+
overall_status = "warning"
|
|
452
|
+
|
|
453
|
+
icon = _status_icon(status)
|
|
454
|
+
style = _status_style(status)
|
|
455
|
+
col = _color(
|
|
456
|
+
result["last_observed"],
|
|
457
|
+
**METRIC_THRESHOLDS.get(metric, {"warning": 75.0, "critical": 90.0}),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
console.print(
|
|
461
|
+
f" {icon} [cyan]{metric:<20}[/cyan] "
|
|
462
|
+
f"[{style}]{status.upper():<10}[/{style}] "
|
|
463
|
+
f"now: [{col}]{result['last_observed']}%[/{col}] "
|
|
464
|
+
f"threshold: {threshold}% "
|
|
465
|
+
f"trend: {result['trend_summary']}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
overall_icon = _status_icon(overall_status)
|
|
469
|
+
overall_style = _status_style(overall_status)
|
|
470
|
+
|
|
471
|
+
console.print(
|
|
472
|
+
f"\n {overall_icon} "
|
|
473
|
+
f"Overall: [{overall_style}]{overall_status.upper()}[/{overall_style}]\n"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
if __name__ == "__main__":
|
|
478
|
+
app()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import psutil
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import time
|
|
4
|
+
from foresight.storage import save_snapshot
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def collect_snapshot() -> dict:
|
|
8
|
+
cpu = psutil.cpu_percent(interval=1)
|
|
9
|
+
|
|
10
|
+
ram = psutil.virtual_memory()
|
|
11
|
+
ram_used_percent = ram.percent
|
|
12
|
+
ram_used_mb = ram.used / (1024 ** 2)
|
|
13
|
+
ram_total_mb = ram.total / (1024 ** 2)
|
|
14
|
+
|
|
15
|
+
disk = psutil.disk_usage("/")
|
|
16
|
+
disk_used_percent = disk.percent
|
|
17
|
+
disk_used_gb = disk.used / (1024 ** 3)
|
|
18
|
+
disk_total_gb = disk.total / (1024 ** 3)
|
|
19
|
+
|
|
20
|
+
timestamp = datetime.now().isoformat()
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"timestamp": timestamp,
|
|
24
|
+
"cpu_percent": cpu,
|
|
25
|
+
"ram_percent": ram_used_percent,
|
|
26
|
+
"ram_used_mb": round(ram_used_mb, 2),
|
|
27
|
+
"ram_total_mb": round(ram_total_mb, 2),
|
|
28
|
+
"disk_percent": disk_used_percent,
|
|
29
|
+
"disk_used_gb": round(disk_used_gb, 2),
|
|
30
|
+
"disk_total_gb": round(disk_total_gb, 2),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def collect_loop(interval_seconds: int = 60, rounds: int = 60) -> None:
|
|
35
|
+
print(f"Collecting every {interval_seconds}s for {rounds} rounds...\n")
|
|
36
|
+
for i in range(rounds):
|
|
37
|
+
snapshot = collect_snapshot()
|
|
38
|
+
save_snapshot(snapshot)
|
|
39
|
+
print(f"[Round {i+1}] {snapshot['timestamp']}")
|
|
40
|
+
print(f" CPU : {snapshot['cpu_percent']}%")
|
|
41
|
+
print(f" RAM : {snapshot['ram_percent']}% ({snapshot['ram_used_mb']} MB used)")
|
|
42
|
+
print(f" Disk : {snapshot['disk_percent']}% ({snapshot['disk_used_gb']} GB used)\n")
|
|
43
|
+
if i < rounds - 1:
|
|
44
|
+
time.sleep(interval_seconds)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
from foresight.storage import init_db
|
|
49
|
+
init_db()
|
|
50
|
+
collect_loop(interval_seconds=5, rounds=3)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from statsmodels.tsa.arima.model import ARIMA
|
|
4
|
+
from statsmodels.tsa.holtwinters import ExponentialSmoothing
|
|
5
|
+
from foresight.storage import get_metric_series, VALID_METRICS
|
|
6
|
+
|
|
7
|
+
warnings.filterwarnings("ignore")
|
|
8
|
+
|
|
9
|
+
MINIMUM_DATA_POINTS = 20
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ─── Private Helpers ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
def _validate_metric(metric: str) -> None:
|
|
15
|
+
if metric not in VALID_METRICS:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Invalid metric '{metric}'. "
|
|
18
|
+
f"Choose from: {VALID_METRICS}"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_series(metric: str, limit: int) -> pd.Series:
|
|
23
|
+
timestamps, values = get_metric_series(metric=metric, limit=limit)
|
|
24
|
+
|
|
25
|
+
if len(values) < MINIMUM_DATA_POINTS:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Not enough data. Need at least {MINIMUM_DATA_POINTS} "
|
|
28
|
+
f"snapshots, got {len(values)}. "
|
|
29
|
+
f"Run 'foresight collect' to gather more data."
|
|
30
|
+
)
|
|
31
|
+
# Soft warning — good forecast needs 3x the requested limit
|
|
32
|
+
if len(values) < limit * 0.5:
|
|
33
|
+
import warnings
|
|
34
|
+
warnings.warn(
|
|
35
|
+
f"Only {len(values)} snapshots available. "
|
|
36
|
+
f"Forecasts improve significantly with more data. "
|
|
37
|
+
f"Run 'foresight collect --rounds 60' for best results.",
|
|
38
|
+
UserWarning,
|
|
39
|
+
stacklevel=2,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return pd.Series(values)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _describe_trend(forecast_values: list[float]) -> str:
|
|
46
|
+
first = forecast_values[0]
|
|
47
|
+
last = forecast_values[-1]
|
|
48
|
+
delta = last - first
|
|
49
|
+
|
|
50
|
+
if delta > 5:
|
|
51
|
+
return "rising"
|
|
52
|
+
elif delta < -5:
|
|
53
|
+
return "falling"
|
|
54
|
+
else:
|
|
55
|
+
return "stable"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─── Forecasting Models ───────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def forecast_arima(
|
|
61
|
+
metric: str = "cpu_percent",
|
|
62
|
+
steps: int = 10,
|
|
63
|
+
limit: int = 100,
|
|
64
|
+
order: tuple = (2, 1, 1),
|
|
65
|
+
) -> dict:
|
|
66
|
+
_validate_metric(metric)
|
|
67
|
+
series = _load_series(metric=metric, limit=limit)
|
|
68
|
+
|
|
69
|
+
model = ARIMA(series, order=order)
|
|
70
|
+
fitted = model.fit()
|
|
71
|
+
|
|
72
|
+
forecast_result = fitted.forecast(steps=steps)
|
|
73
|
+
forecast_values = [round(float(v), 2) for v in forecast_result]
|
|
74
|
+
forecast_values = [max(0.0, min(100.0, v)) for v in forecast_values]
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"metric": metric,
|
|
78
|
+
"model": "ARIMA",
|
|
79
|
+
"order": order,
|
|
80
|
+
"last_observed": round(float(series.iloc[-1]), 2),
|
|
81
|
+
"steps_ahead": steps,
|
|
82
|
+
"forecast": forecast_values,
|
|
83
|
+
"trend_summary": _describe_trend(forecast_values),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def forecast_holtwinters(
|
|
88
|
+
metric: str = "cpu_percent",
|
|
89
|
+
steps: int = 10,
|
|
90
|
+
limit: int = 100,
|
|
91
|
+
) -> dict:
|
|
92
|
+
_validate_metric(metric)
|
|
93
|
+
series = _load_series(metric=metric, limit=limit)
|
|
94
|
+
|
|
95
|
+
model = ExponentialSmoothing(
|
|
96
|
+
series,
|
|
97
|
+
trend="add",
|
|
98
|
+
seasonal=None,
|
|
99
|
+
initialization_method="estimated",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
fitted = model.fit(optimized=True)
|
|
103
|
+
forecast_result = fitted.forecast(steps=steps)
|
|
104
|
+
|
|
105
|
+
forecast_values = [
|
|
106
|
+
round(max(0.0, min(100.0, float(v))), 2)
|
|
107
|
+
for v in forecast_result
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"metric": metric,
|
|
112
|
+
"model": "Holt-Winters",
|
|
113
|
+
"order": None,
|
|
114
|
+
"last_observed": round(float(series.iloc[-1]), 2),
|
|
115
|
+
"steps_ahead": steps,
|
|
116
|
+
"forecast": forecast_values,
|
|
117
|
+
"trend_summary": _describe_trend(forecast_values),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def forecast_ensemble(
|
|
122
|
+
metric: str = "cpu_percent",
|
|
123
|
+
steps: int = 10,
|
|
124
|
+
limit: int = 100,
|
|
125
|
+
) -> dict:
|
|
126
|
+
_validate_metric(metric)
|
|
127
|
+
|
|
128
|
+
arima_result = forecast_arima(metric=metric, steps=steps, limit=limit)
|
|
129
|
+
hw_result = forecast_holtwinters(metric=metric, steps=steps, limit=limit)
|
|
130
|
+
|
|
131
|
+
blended = [
|
|
132
|
+
round(max(0.0, min(100.0, (a + b) / 2)), 2)
|
|
133
|
+
for a, b in zip(arima_result["forecast"], hw_result["forecast"])
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"metric": metric,
|
|
138
|
+
"model": "Ensemble (ARIMA + Holt-Winters)",
|
|
139
|
+
"order": None,
|
|
140
|
+
"last_observed": arima_result["last_observed"],
|
|
141
|
+
"steps_ahead": steps,
|
|
142
|
+
"forecast": blended,
|
|
143
|
+
"trend_summary": _describe_trend(blended),
|
|
144
|
+
"components": {
|
|
145
|
+
"arima": arima_result["forecast"],
|
|
146
|
+
"holtwinters": hw_result["forecast"],
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ─── Alerting ─────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def check_threshold(
|
|
154
|
+
forecast_result: dict,
|
|
155
|
+
threshold: float = 85.0,
|
|
156
|
+
) -> dict:
|
|
157
|
+
forecast_values = forecast_result["forecast"]
|
|
158
|
+
metric = forecast_result["metric"]
|
|
159
|
+
last_observed = forecast_result["last_observed"]
|
|
160
|
+
|
|
161
|
+
breaches = [
|
|
162
|
+
{"step": i + 1, "value": v}
|
|
163
|
+
for i, v in enumerate(forecast_values)
|
|
164
|
+
if v >= threshold
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
already_critical = last_observed >= threshold
|
|
168
|
+
|
|
169
|
+
if already_critical:
|
|
170
|
+
status = "critical"
|
|
171
|
+
message = (
|
|
172
|
+
f"{metric} is ALREADY at {last_observed}% — "
|
|
173
|
+
f"above threshold of {threshold}%"
|
|
174
|
+
)
|
|
175
|
+
elif breaches:
|
|
176
|
+
first_breach = breaches[0]
|
|
177
|
+
status = "warning"
|
|
178
|
+
message = (
|
|
179
|
+
f"{metric} predicted to reach {first_breach['value']}% "
|
|
180
|
+
f"in {steps_to_human_time(first_breach['step'])} "
|
|
181
|
+
f"(threshold: {threshold}%)"
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
status = "ok"
|
|
185
|
+
message = (
|
|
186
|
+
f"{metric} looks safe — "
|
|
187
|
+
f"no breach predicted within {len(forecast_values)} steps "
|
|
188
|
+
f"(threshold: {threshold}%)"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"metric": metric,
|
|
193
|
+
"threshold": threshold,
|
|
194
|
+
"status": status,
|
|
195
|
+
"message": message,
|
|
196
|
+
"breaches": breaches,
|
|
197
|
+
"already_critical": already_critical,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ─── Time Utilities ───────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def steps_to_human_time(step: int, interval_seconds: int = 60) -> str:
|
|
204
|
+
total_seconds = step * interval_seconds
|
|
205
|
+
total_minutes = total_seconds // 60
|
|
206
|
+
|
|
207
|
+
if total_minutes < 1:
|
|
208
|
+
return f"~{total_seconds}s"
|
|
209
|
+
elif total_minutes < 60:
|
|
210
|
+
return f"~{total_minutes}m"
|
|
211
|
+
else:
|
|
212
|
+
hours = total_minutes // 60
|
|
213
|
+
minutes = total_minutes % 60
|
|
214
|
+
if minutes == 0:
|
|
215
|
+
return f"~{hours}h"
|
|
216
|
+
return f"~{hours}h {minutes}m"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def parse_horizon(horizon: str, interval_seconds: int = 60) -> int:
|
|
220
|
+
horizon = horizon.strip().lower()
|
|
221
|
+
|
|
222
|
+
if horizon.endswith("h"):
|
|
223
|
+
hours = float(horizon[:-1])
|
|
224
|
+
total_seconds = hours * 3600
|
|
225
|
+
elif horizon.endswith("m"):
|
|
226
|
+
minutes = float(horizon[:-1])
|
|
227
|
+
total_seconds = minutes * 60
|
|
228
|
+
else:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"Invalid horizon '{horizon}'. "
|
|
231
|
+
f"Use format: '30m' for 30 minutes or '2h' for 2 hours."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
steps = max(1, int(total_seconds // interval_seconds))
|
|
235
|
+
return steps
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
result = forecast_arima(metric="cpu_percent", steps=10)
|
|
240
|
+
print(f"\nMetric : {result['metric']}")
|
|
241
|
+
print(f"Model : {result['model']} {result['order']}")
|
|
242
|
+
print(f"Last seen: {result['last_observed']}%")
|
|
243
|
+
print(f"Trend : {result['trend_summary']}")
|
|
244
|
+
print(f"Forecast : {result['forecast']}")
|
|
245
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
DB_DIR = Path(__file__).parent.parent / "data"
|
|
6
|
+
DB_PATH = DB_DIR / "foresight.db"
|
|
7
|
+
|
|
8
|
+
VALID_METRICS = {
|
|
9
|
+
"cpu_percent",
|
|
10
|
+
"ram_percent",
|
|
11
|
+
"ram_used_mb",
|
|
12
|
+
"disk_percent",
|
|
13
|
+
"disk_used_gb",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def init_db() -> None:
|
|
18
|
+
DB_DIR.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
conn = sqlite3.connect(DB_PATH)
|
|
21
|
+
cursor = conn.cursor()
|
|
22
|
+
|
|
23
|
+
cursor.execute("""
|
|
24
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
timestamp TEXT NOT NULL,
|
|
27
|
+
cpu_percent REAL NOT NULL,
|
|
28
|
+
ram_percent REAL NOT NULL,
|
|
29
|
+
ram_used_mb REAL NOT NULL,
|
|
30
|
+
ram_total_mb REAL NOT NULL,
|
|
31
|
+
disk_percent REAL NOT NULL,
|
|
32
|
+
disk_used_gb REAL NOT NULL,
|
|
33
|
+
disk_total_gb REAL NOT NULL
|
|
34
|
+
)
|
|
35
|
+
""")
|
|
36
|
+
|
|
37
|
+
conn.commit()
|
|
38
|
+
conn.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_snapshot(snapshot: dict) -> None:
|
|
42
|
+
conn = sqlite3.connect(DB_PATH)
|
|
43
|
+
cursor = conn.cursor()
|
|
44
|
+
|
|
45
|
+
cursor.execute("""
|
|
46
|
+
INSERT INTO snapshots (
|
|
47
|
+
timestamp,
|
|
48
|
+
cpu_percent,
|
|
49
|
+
ram_percent,
|
|
50
|
+
ram_used_mb,
|
|
51
|
+
ram_total_mb,
|
|
52
|
+
disk_percent,
|
|
53
|
+
disk_used_gb,
|
|
54
|
+
disk_total_gb
|
|
55
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
56
|
+
""", (
|
|
57
|
+
snapshot["timestamp"],
|
|
58
|
+
snapshot["cpu_percent"],
|
|
59
|
+
snapshot["ram_percent"],
|
|
60
|
+
snapshot["ram_used_mb"],
|
|
61
|
+
snapshot["ram_total_mb"],
|
|
62
|
+
snapshot["disk_percent"],
|
|
63
|
+
snapshot["disk_used_gb"],
|
|
64
|
+
snapshot["disk_total_gb"],
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
conn.commit()
|
|
68
|
+
conn.close()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_snapshots(limit: int = 100) -> list[dict]:
|
|
72
|
+
conn = sqlite3.connect(DB_PATH)
|
|
73
|
+
conn.row_factory = sqlite3.Row
|
|
74
|
+
cursor = conn.cursor()
|
|
75
|
+
|
|
76
|
+
cursor.execute("""
|
|
77
|
+
SELECT * FROM snapshots
|
|
78
|
+
ORDER BY timestamp DESC
|
|
79
|
+
LIMIT ?
|
|
80
|
+
""", (limit,))
|
|
81
|
+
|
|
82
|
+
rows = cursor.fetchall()
|
|
83
|
+
conn.close()
|
|
84
|
+
|
|
85
|
+
return [dict(row) for row in rows]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_metric_series(metric: str, limit: int = 50) -> tuple[list, list]:
|
|
89
|
+
if metric not in VALID_METRICS:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Invalid metric '{metric}'. "
|
|
92
|
+
f"Choose from: {VALID_METRICS}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
conn = sqlite3.connect(DB_PATH)
|
|
96
|
+
conn.row_factory = sqlite3.Row
|
|
97
|
+
cursor = conn.cursor()
|
|
98
|
+
|
|
99
|
+
cursor.execute(f"""
|
|
100
|
+
SELECT timestamp, {metric}
|
|
101
|
+
FROM snapshots
|
|
102
|
+
ORDER BY timestamp DESC
|
|
103
|
+
LIMIT ?
|
|
104
|
+
""", (limit,))
|
|
105
|
+
|
|
106
|
+
rows = cursor.fetchall()
|
|
107
|
+
conn.close()
|
|
108
|
+
|
|
109
|
+
rows = list(reversed(rows))
|
|
110
|
+
timestamps = [row["timestamp"] for row in rows]
|
|
111
|
+
values = [row[metric] for row in rows]
|
|
112
|
+
|
|
113
|
+
return timestamps, values
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
init_db()
|
|
118
|
+
print(f"Database initialized at: {DB_PATH}")
|
|
119
|
+
|
|
120
|
+
dummy = {
|
|
121
|
+
"timestamp": "2026-03-22T10:00:00",
|
|
122
|
+
"cpu_percent": 45.2,
|
|
123
|
+
"ram_percent": 61.0,
|
|
124
|
+
"ram_used_mb": 9800.0,
|
|
125
|
+
"ram_total_mb": 16384.0,
|
|
126
|
+
"disk_percent": 43.0,
|
|
127
|
+
"disk_used_gb": 430.0,
|
|
128
|
+
"disk_total_gb": 953.0,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
save_snapshot(dummy)
|
|
132
|
+
print("Saved one dummy snapshot.")
|
|
133
|
+
|
|
134
|
+
results = get_snapshots(limit=5)
|
|
135
|
+
print(f"\nLast {len(results)} snapshots:")
|
|
136
|
+
for row in results:
|
|
137
|
+
print(row)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foresight-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Predict system resource exhaustion before it happens.
|
|
5
|
+
Author: Rishi Garg
|
|
6
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/foresight
|
|
7
|
+
Project-URL: Repository, https://github.com/YOUR_USERNAME/foresight
|
|
8
|
+
Project-URL: Issues, https://github.com/YOUR_USERNAME/foresight/issues
|
|
9
|
+
Keywords: cli,monitoring,forecasting,devops,time-series,arima,machine-learning,psutil,system-monitoring
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Intended Audience :: System Administrators
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: psutil>=5.9.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Requires-Dist: typer>=0.9.0
|
|
25
|
+
Requires-Dist: plotext>=5.2.0
|
|
26
|
+
Requires-Dist: pandas>=2.0.0
|
|
27
|
+
Requires-Dist: statsmodels>=0.14.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
foresight/__init__.py
|
|
4
|
+
foresight/cli.py
|
|
5
|
+
foresight/collector.py
|
|
6
|
+
foresight/forecaster.py
|
|
7
|
+
foresight/storage.py
|
|
8
|
+
foresight_cli.egg-info/PKG-INFO
|
|
9
|
+
foresight_cli.egg-info/SOURCES.txt
|
|
10
|
+
foresight_cli.egg-info/dependency_links.txt
|
|
11
|
+
foresight_cli.egg-info/entry_points.txt
|
|
12
|
+
foresight_cli.egg-info/requires.txt
|
|
13
|
+
foresight_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
foresight
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
[project]
|
|
5
|
+
name = "foresight-cli"
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
description = "Predict system resource exhaustion before it happens."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Rishi Garg" }
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"cli", "monitoring", "forecasting", "devops",
|
|
16
|
+
"time-series", "arima", "machine-learning",
|
|
17
|
+
"psutil", "system-monitoring"
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Topic :: System :: Monitoring",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Intended Audience :: System Administrators",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"psutil>=5.9.0",
|
|
33
|
+
"rich>=13.0.0",
|
|
34
|
+
"typer>=0.9.0",
|
|
35
|
+
"plotext>=5.2.0",
|
|
36
|
+
"pandas>=2.0.0",
|
|
37
|
+
"statsmodels>=0.14.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/YOUR_USERNAME/foresight"
|
|
42
|
+
Repository = "https://github.com/YOUR_USERNAME/foresight"
|
|
43
|
+
Issues = "https://github.com/YOUR_USERNAME/foresight/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
foresight = "foresight.cli:app"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["."]
|
|
50
|
+
include = ["foresight*"]
|
|
51
|
+
|