diskrx 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: diskrx
3
+ Version: 0.1.0
4
+ Summary: Intelligent disk diagnostics and storage observability for SREs
5
+ Author-email: Seshadri Naidu Vangapandu <seshuvangapandu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Seshadri724/dxcli
8
+ Project-URL: Repository, https://github.com/Seshadri724/dxcli
9
+ Project-URL: Issues, https://github.com/Seshadri724/dxcli/issues
10
+ Keywords: disk,diagnostics,sre,observability,storage,cli
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: System :: Monitoring
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: click>=8.0
25
+ Requires-Dist: rich>=13.0
26
+ Requires-Dist: textual>=0.50
27
+ Requires-Dist: psutil>=5.9.0
28
+ Requires-Dist: numpy>=1.24.0
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=7.0.0; extra == "test"
31
+ Requires-Dist: pytest-mock>=3.10.0; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ # 🩺 dxcli — The Disk Doctor
35
+
36
+ > **Stop firefighting. Start predicting.**
37
+ > Replace your 45-minute disk investigation with a 30-second diagnosis.
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/dxcli.svg)](https://pypi.org/project/dxcli/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/dxcli.svg)](https://pypi.org/project/dxcli/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
42
+ [![Downloads](https://img.shields.io/pypi/dm/dxcli)](https://pypi.org/project/dxcli/)
43
+
44
+ ---
45
+
46
+ ## The Problem
47
+
48
+ It's 2 AM. PagerDuty fires. Your production server is at 98% disk.
49
+
50
+ You SSH in and start the ritual:
51
+
52
+ ```bash
53
+ df -h # okay, it's /var
54
+ du -sh /var/* # narrowing down...
55
+ du -sh /var/log/* | sort -h # getting warmer...
56
+ find /var/log -size +100M # which file?
57
+ lsof | grep deleted # which process?!
58
+ ```
59
+
60
+ **45 minutes later**, you've found the culprit — a runaway log file from a service nobody knew was deployed. You delete it, go back to sleep, and it happens again next week.
61
+
62
+ **There is a better way.**
63
+
64
+ ---
65
+
66
+ ## The Solution
67
+
68
+ ```bash
69
+ dxcli diagnose /var
70
+ ```
71
+
72
+ ```
73
+ 🩺 Disk Doctor — Diagnosis Report
74
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
75
+
76
+ 🚨 LOG BOMB DETECTED
77
+ /var/log/payments/transaction.log — 14.2 GB
78
+ ↳ Written by: payments-service (PID 18423)
79
+ ↳ Last rotation: Never
80
+ ↳ Prescription: Add logrotate config immediately
81
+
82
+ ⚠️ STALE DATA
83
+ /var/cache/thumbnails — 8.7 GB
84
+ ↳ Last accessed: 94 days ago
85
+ ↳ Prescription: Safe to archive or delete
86
+
87
+ 📈 GROWTH FORECAST
88
+ At current rate, /var fills in 6 hours 12 minutes
89
+ Growth is ACCELERATING (+340% vs last week)
90
+ ```
91
+
92
+ **30 seconds. Exact culprit. Exact prescription.**
93
+
94
+ ---
95
+
96
+ ## Features
97
+
98
+ ### 🔍 `dxcli diagnose` — Intelligent Diagnosis
99
+ Not just file sizes. It tells you **why** your disk is filling:
100
+ - **Log Bomb detection** — unrotated logs growing out of control
101
+ - **Stale file identification** — large files untouched for months
102
+ - **Process attribution** — exactly which PID is writing to a path
103
+
104
+ ### 📈 `dxcli predict` — Time-to-Full Forecasting
105
+ Linear regression on historical snapshots stored locally. Tells you:
106
+ - When your disk will be full (hours, days, weeks)
107
+ - Whether growth is stable or **accelerating**
108
+ - Which directories are the fastest-growing threats
109
+
110
+ ### 🖥️ `dxcli dash` — Real-time TUI Dashboard
111
+ A full terminal UI with live sparklines, anomaly alerts, and interactive process maps. No browser required.
112
+
113
+ ```
114
+ ┌─ Disk Overview ─────────────────────────────────┐
115
+ │ /var [████████████████████░░░░] 84% ↑ FAST │
116
+ │ /home [████████░░░░░░░░░░░░░░░░] 34% → STABLE│
117
+ │ /tmp [███░░░░░░░░░░░░░░░░░░░░░] 12% ↓ SLOW │
118
+ │ │
119
+ │ 🚨 ANOMALY: Log Bomb in /var/log/payments │
120
+ └──────────────────────────────────────────────────┘
121
+ ```
122
+
123
+ ### 🔭 `dxcli serve` — The Sentinel (Prometheus-compatible)
124
+ Run as a background daemon. Exports `/metrics` for Grafana integration. Plug dxcli's intelligence directly into your existing observability stack.
125
+
126
+ ---
127
+
128
+ ## Installation
129
+
130
+ ```bash
131
+ pip install dxcli
132
+ ```
133
+
134
+ That's it. No config files. No daemons required to get started.
135
+
136
+ ---
137
+
138
+ ## Quickstart
139
+
140
+ ```bash
141
+ # Diagnose a path right now
142
+ dxcli diagnose /var
143
+
144
+ # Predict when your root partition fills up
145
+ dxcli predict /
146
+
147
+ # Open the live TUI dashboard
148
+ dxcli dash
149
+
150
+ # Start the Prometheus metrics server
151
+ dxcli serve --port 8000
152
+ ```
153
+
154
+ ---
155
+
156
+ ## How It Works
157
+
158
+ dxcli stores lightweight disk snapshots in a local SQLite database (`~/.dx/history.db`). Over time, it builds a picture of your disk's growth patterns and uses linear regression to forecast the future.
159
+
160
+ ```
161
+ ┌─────────────────────────────────────────────────────────┐
162
+ │ dxcli Architecture │
163
+ ├──────────────┬──────────────┬────────────┬─────────────┤
164
+ │ collectors/ │ analyzers/ │ store/ │ outputs/ │
165
+ │ │ │ │ │
166
+ │ Raw OS data │ The "Brain" │ SQLite DB │ Rich / TUI │
167
+ │ psutil │ Growth rate │ Snapshots │ Prometheus │
168
+ │ File scans │ Anomaly │ History │ HTTP API │
169
+ │ PID mapping │ detection │ │ │
170
+ └──────────────┴──────────────┴────────────┴─────────────┘
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Who This Is For
176
+
177
+ - **SREs** who are tired of getting paged for disk full alerts they could have predicted
178
+ - **DevOps engineers** who want disk intelligence in their Grafana dashboards
179
+ - **Platform engineers** who need to attribute storage costs to specific services
180
+ - **Anyone** who has typed `du -sh * | sort -h` more than once this month
181
+
182
+ ---
183
+
184
+ ## Integrations
185
+
186
+ **Grafana / Prometheus**
187
+
188
+ Add to your `prometheus.yml`:
189
+ ```yaml
190
+ scrape_configs:
191
+ - job_name: 'dxcli'
192
+ static_configs:
193
+ - targets: ['localhost:8000']
194
+ ```
195
+
196
+ Then import the dxcli dashboard from Grafana marketplace. *(Coming soon)*
197
+
198
+ ---
199
+
200
+ ## Roadmap
201
+
202
+ - [ ] Grafana dashboard JSON export
203
+ - [ ] Slack / PagerDuty alert webhooks
204
+ - [ ] Multi-host support via SSH
205
+ - [ ] Docker volume awareness
206
+ - [ ] Cloud storage integration (S3 cost attribution)
207
+
208
+ Have a feature request? [Open an issue](https://github.com/your-username/dxcli/issues).
209
+
210
+ ---
211
+
212
+ ## Contributing
213
+
214
+ Contributions are welcome. Please read the contributing guide before submitting a PR.
215
+
216
+ ```bash
217
+ git clone https://github.com/your-username/dxcli
218
+ cd dxcli
219
+ python -m venv venv && source venv/bin/activate
220
+ pip install -e ".[test]"
221
+ pytest tests/ -v
222
+ ```
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ MIT — see [LICENSE](LICENSE) for details.
229
+
230
+ ---
231
+
232
+ <p align="center">
233
+ Built for SREs, by someone who got paged one too many times at 2 AM.
234
+ </p>
@@ -0,0 +1,33 @@
1
+ diskrx-0.1.0.dist-info/licenses/LICENSE,sha256=c86TVJSMxIYa5MVAOiXlAkWw5dxf4my46bpPHew0jPA,1066
2
+ dxcli/__init__.py,sha256=JgE8MwMm8mO6Yxz4hsJBTb8TTlNgMTmRdz_-d0U-Fgg,76
3
+ dxcli/cli.py,sha256=U4pIudskuxs_JDYQGtSBgIr1FccrM9Mz-J7HXoYMsN4,8792
4
+ dxcli/config.py,sha256=GK17rmhIw2oWurfZIL_6Ia56GmirnCI-zm0LO3nghbo,376
5
+ dxcli/analyzers/__init__.py,sha256=K7J4u87hagG3_rOTyb_iR4OmoQ93T9fTMoJR9fPbX2A,394
6
+ dxcli/analyzers/anomaly.py,sha256=wksJizFtkjcOSX2c1G6OncdHVMbsM354gNdYGNn_9Q8,2206
7
+ dxcli/analyzers/correlation.py,sha256=nYy0eyXg6Hz1Pza1bkvpUzdHMOWHBKbwFaPUknWcC68,1107
8
+ dxcli/analyzers/growth.py,sha256=oVn-6EFLcW3qMtWNcBEeSnpNBaS4VC_ep64g2XRpaZw,2014
9
+ dxcli/analyzers/predictor.py,sha256=MhTAYlYXJ_OPTlOSDFjGGLzGOyzUWN2tnLEtp8eoPzg,1914
10
+ dxcli/analyzers/prescriptions.py,sha256=vrM5stRtfETeRWMkQQOruBR37ZuIsBknqEL_bpAbjg0,1967
11
+ dxcli/analyzers/root_cause.py,sha256=qAnbs0Kr3JPCF_bmZ6XwKCXN_qaPyDe22FtKiB7bEvc,1480
12
+ dxcli/collectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ dxcli/collectors/dir_tree.py,sha256=6FkI_5ndW-JZNUVoJE9XVV9leTQerwbJLvv5wsrzXfU,2191
14
+ dxcli/collectors/log_finder.py,sha256=z1GAQMoWr_HPDvn46qwciEYyw-Vc8cmCK6cx5DxHPqk,2073
15
+ dxcli/collectors/process_mapper.py,sha256=2Qavu4gEb6tJBgjv-L6AUv-oHNAM6X1orCTXU097ulI,2236
16
+ dxcli/collectors/stale_files.py,sha256=6hvMWj431yhhao5_V3wG72rT0GCtIDqPhGxqourC02k,1771
17
+ dxcli/outputs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ dxcli/outputs/cli_report.py,sha256=jpUimvxGtLOp1YA25KBUs88UXmGWcePLyPFYNLalFiQ,9592
19
+ dxcli/outputs/metrics.py,sha256=-V6sM3AH5XUq7qiY1Db-UPCEqamdyXSyfkidFNz1UE0,1665
20
+ dxcli/outputs/tui.py,sha256=H3U4KS7cmTOuQYP9BDddLl85uKnq_E6l7C1QSh4r3Dc,10501
21
+ dxcli/outputs/tui.tcss,sha256=cSjyPxi9WqDp7p-HgCXdxPfIER7jQCJ4f4bdUl0rXw0,949
22
+ dxcli/platform/__init__.py,sha256=lhEMYtqI8Cc4fCtoc7d8mKy4Jk7aqHCStHuPpZZc_QQ,380
23
+ dxcli/platform/base.py,sha256=ika45WXYexF6ud2YbgIt5U2y0OARi0K_lzSRzQhovMk,288
24
+ dxcli/platform/linux.py,sha256=IV1yUIl4UwwK-IvWkUG2L5kN38Acw9QemDsXip_dNYU,1001
25
+ dxcli/platform/windows.py,sha256=IfDhZUQ0r8opZmXgeIL9xvrzGT1EaxBsjdjAmZR4NPE,966
26
+ dxcli/store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ dxcli/store/database.py,sha256=mdkpoT9QuRGEq8k3XieXYV9Gqu6Tkf-3HHWDYucxAWw,4335
28
+ dxcli/store/models.py,sha256=9fsYAkX-n5XKllUv_ZR20HEIlkTxL-KEht1KaV63buU,1145
29
+ diskrx-0.1.0.dist-info/METADATA,sha256=9IgESh6mpQB0DUboNeya213WWQkiy4L3nwzJYdeFoB4,8015
30
+ diskrx-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
31
+ diskrx-0.1.0.dist-info/entry_points.txt,sha256=snu1liBb92kyziSW6uOscR6GG-uFz1QdxKGbXK48xwg,40
32
+ diskrx-0.1.0.dist-info/top_level.txt,sha256=_KM7grJMzPfpjdmzxvfT4RpFqLpljGEPFRK4IErTeug,6
33
+ diskrx-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dxcli = dxcli.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Developer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dxcli
dxcli/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ dxcli
3
+ Intelligent disk diagnostics for SREs.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,15 @@
1
+ from .growth import GrowthTracker
2
+ from .predictor import DiskPredictor
3
+ from .root_cause import RootCauseAnalyzer
4
+ from .prescriptions import PrescriptionEngine
5
+ from .correlation import CorrelationEngine
6
+ from .anomaly import AnomalyDetector
7
+
8
+ __all__ = [
9
+ "GrowthTracker",
10
+ "DiskPredictor",
11
+ "RootCauseAnalyzer",
12
+ "PrescriptionEngine",
13
+ "CorrelationEngine",
14
+ "AnomalyDetector"
15
+ ]
@@ -0,0 +1,58 @@
1
+ from typing import List, Dict, Optional
2
+ from ..store.database import Database
3
+ import time
4
+
5
+ class AnomalyDetector:
6
+ """
7
+ Analyzes historical snapshots to fingerprint behavioral anomalies.
8
+ """
9
+ def __init__(self, db: Database):
10
+ self.db = db
11
+
12
+ def check_for_anomalies(self, path: str) -> Optional[str]:
13
+ """
14
+ Main entry point for fingerprinting.
15
+ Returns a descriptive string if an anomaly is found, else None.
16
+ """
17
+ history = self.db.get_dir_history(path, limit=5) # Last 5 points
18
+ if len(history) < 3:
19
+ return None
20
+
21
+ # 1. Check for Log Bomb (Rapid, sudden, sustained acceleration)
22
+ if self._is_log_bomb(history):
23
+ return "LOG BOMB: Rapid, sustained write spike detected."
24
+
25
+ # 2. Check for Persistent Leak (Small but never-ending growth)
26
+ if self._is_leak(history):
27
+ return "LEAK: Steady growth over several hours with no cleanup."
28
+
29
+ return None
30
+
31
+ def _is_log_bomb(self, history: List[Dict]) -> bool:
32
+ # Simplistic check: If last growth is > 5x the average of previous points
33
+ deltas = []
34
+ for i in range(len(history)-1):
35
+ deltas.append(max(0, history[i+1]['size_bytes'] - history[i]['size_bytes']))
36
+
37
+ if len(deltas) < 2: return False
38
+
39
+ last_delta = deltas[-1]
40
+ avg_prev_delta = sum(deltas[:-1]) / len(deltas[:-1])
41
+
42
+ # If it's a massive jump (e.g., 10x) and substantial (>10MB)
43
+ if last_delta > (avg_prev_delta * 10) and last_delta > 1024 * 1024 * 10:
44
+ return True
45
+ return False
46
+
47
+ def _is_leak(self, history: List[Dict]) -> bool:
48
+ # Check if EVERY delta in the last 5 points is positive (no drops)
49
+ deltas = []
50
+ for i in range(len(history)-1):
51
+ deltas.append(history[i+1]['size_bytes'] - history[i]['size_bytes'])
52
+
53
+ if all(d > 0 for d in deltas) and len(deltas) >= 4:
54
+ # If total growth over this time is > 1MB (avoid noise)
55
+ total_change = sum(deltas)
56
+ if total_change > 1024 * 1024:
57
+ return True
58
+ return False
@@ -0,0 +1,32 @@
1
+ from typing import List, Dict
2
+ from ..store.models import DirNode
3
+ from ..collectors.process_mapper import ProcessMapper, ProcessRef
4
+
5
+ class CorrelationEngine:
6
+ """
7
+ Bridges the gap between the growth analyzer and the process list.
8
+ """
9
+ def __init__(self):
10
+ self.mapper = ProcessMapper()
11
+
12
+ def correlate(self, growing_dirs: List[Dict]) -> List[Dict]:
13
+ """
14
+ Takes a list of growth results from RootCauseAnalyzer and attempts
15
+ to attribute a PID to each growing directory.
16
+ """
17
+ results = []
18
+ for g in growing_dirs:
19
+ # We only look for culprits if the trend is not "Stable"
20
+ res = g.copy()
21
+ res["culprit"] = None
22
+
23
+ if g["trend"] != "Stable":
24
+ culprits = self.mapper.find_culprits(g["path"])
25
+ if culprits:
26
+ # Pick the first one or prioritize by name match?
27
+ # For now just the first one found
28
+ res["culprit"] = culprits[0]
29
+
30
+ results.append(res)
31
+
32
+ return results
@@ -0,0 +1,53 @@
1
+ from ..store.database import Database
2
+ from ..store.models import GrowthRate
3
+ from typing import Optional
4
+ import numpy as np
5
+ import warnings
6
+
7
+ class GrowthTracker:
8
+ """
9
+ Calculates daily growth rate per directory based on historical SQLite snapshots.
10
+ """
11
+ def __init__(self, db: Database):
12
+ self.db = db
13
+
14
+ def get_growth_rate(self, path: str, days: int = 7) -> Optional[GrowthRate]:
15
+ history = self.db.get_dir_history(path, days_back=days)
16
+ if len(history) < 2:
17
+ return None # Not enough history
18
+
19
+ # Extract timestamps and sizes
20
+ timestamps = np.array([h['timestamp'] for h in history])
21
+ sizes = np.array([h['size_bytes'] for h in history])
22
+
23
+ # Linear regression: size = m * timestamp + c
24
+ # We want m (bytes per second)
25
+ # Polyfit returns [m, c] for deg=1
26
+ with warnings.catch_warnings():
27
+ warnings.simplefilter('error', np.exceptions.RankWarning)
28
+ try:
29
+ m, _ = np.polyfit(timestamps, sizes, 1)
30
+ except np.exceptions.RankWarning:
31
+ return None # Data too noisy/flat for reliable regression
32
+
33
+ # Convert bytes per second to bytes per day
34
+ bytes_per_day = m * 86400.0
35
+
36
+ return GrowthRate(path=path, bytes_per_day=bytes_per_day)
37
+
38
+ def get_partition_growth_rate(self, mountpoint: str, days: int = 7) -> float:
39
+ """Returns bytes per day growth for a whole partition."""
40
+ history = self.db.get_history(mountpoint, days_back=days)
41
+ if len(history) < 2:
42
+ return 0.0
43
+
44
+ timestamps = np.array([h['timestamp'] for h in history])
45
+ sizes = np.array([h['used_bytes'] for h in history])
46
+
47
+ with warnings.catch_warnings():
48
+ warnings.simplefilter('error', np.exceptions.RankWarning)
49
+ try:
50
+ m, _ = np.polyfit(timestamps, sizes, 1)
51
+ except np.exceptions.RankWarning:
52
+ return 0.0
53
+ return m * 86400.0
@@ -0,0 +1,44 @@
1
+ import time
2
+ from typing import Optional
3
+ from .growth import GrowthTracker
4
+ from ..store.models import PredictionResult, Partition
5
+ from ..store.database import Database
6
+
7
+ class DiskPredictor:
8
+ """
9
+ Linear regression on historical usage data to determine time-to-full.
10
+ """
11
+ def __init__(self, db: Database):
12
+ self.tracker = GrowthTracker(db)
13
+
14
+ def predict_full_date(self, partition: Partition) -> Optional[PredictionResult]:
15
+ daily_growth = self.tracker.get_partition_growth_rate(partition.mountpoint)
16
+
17
+ if daily_growth <= 1024 * 1024: # Less than 1MB/day is considered roughly stable/static
18
+ return PredictionResult(
19
+ path=partition.mountpoint,
20
+ date_full_timestamp=None,
21
+ days_until_full=None,
22
+ current_capacity_bytes=partition.total_bytes,
23
+ current_usage_bytes=partition.used_bytes,
24
+ daily_growth_bytes=daily_growth,
25
+ is_accelerating=False
26
+ )
27
+
28
+ remaining_bytes = partition.total_bytes - partition.used_bytes
29
+ days_until_full = remaining_bytes / daily_growth
30
+
31
+ # Super simple acceleration check (compare last 3 days vs last 7 days)
32
+ short_growth = self.tracker.get_partition_growth_rate(partition.mountpoint, days=3)
33
+ long_growth = self.tracker.get_partition_growth_rate(partition.mountpoint, days=7)
34
+ is_accelerating = short_growth > (long_growth * 1.2) # 20% spike is accelerating
35
+
36
+ return PredictionResult(
37
+ path=partition.mountpoint,
38
+ date_full_timestamp=time.time() + (days_until_full * 86400),
39
+ days_until_full=days_until_full,
40
+ current_capacity_bytes=partition.total_bytes,
41
+ current_usage_bytes=partition.used_bytes,
42
+ daily_growth_bytes=daily_growth,
43
+ is_accelerating=is_accelerating
44
+ )
@@ -0,0 +1,61 @@
1
+ import os
2
+ import sys
3
+ from typing import List
4
+ from ..store.models import Prescription, UnrotatedLog, StaleFile
5
+
6
+ class PrescriptionEngine:
7
+ """
8
+ Pattern-matched remediation recommendations.
9
+ """
10
+ def __init__(self):
11
+ pass
12
+
13
+ def synthesize(self, logs: List[UnrotatedLog], stales: List[StaleFile]) -> List[Prescription]:
14
+ prescriptions = []
15
+ is_windows = sys.platform == "win32"
16
+
17
+ # Log Prescriptions
18
+ for i, log in enumerate(logs):
19
+ if not log.has_logrotate_config:
20
+ service_name = os.path.basename(log.path).replace('.log', '')
21
+ if not service_name:
22
+ service_name = "custom_service"
23
+
24
+ if is_windows:
25
+ template = f"# Windows does not have logrotate.\n# Manually archive or delete: {log.path}"
26
+ name = f"Archive log for {service_name}"
27
+ else:
28
+ template = f"""# /etc/logrotate.d/{service_name}
29
+ {log.path} {{
30
+ daily
31
+ rotate 7
32
+ compress
33
+ missingok
34
+ copytruncate
35
+ }}"""
36
+ name = f"Add logrotate for {service_name}"
37
+
38
+ prescriptions.append(Prescription(
39
+ id=f"log_{i}",
40
+ name=name,
41
+ template=template,
42
+ risk="safe",
43
+ size_savings_bytes=log.size_bytes
44
+ ))
45
+
46
+ # Stale Prescriptions
47
+ for i, stale in enumerate(stales):
48
+ if is_windows:
49
+ cmd = f"Remove-Item -Force '{stale.path}'"
50
+ else:
51
+ cmd = f"rm -f '{stale.path}'"
52
+
53
+ prescriptions.append(Prescription(
54
+ id=f"stale_{i}",
55
+ name=f"Remove stale file: {os.path.basename(stale.path)}",
56
+ template=cmd,
57
+ risk="needs-review",
58
+ size_savings_bytes=stale.size_bytes
59
+ ))
60
+
61
+ return prescriptions
@@ -0,0 +1,41 @@
1
+ from typing import List, Dict
2
+ from ..store.models import DirNode
3
+ from .growth import GrowthTracker
4
+ from ..store.database import Database
5
+
6
+ class RootCauseAnalyzer:
7
+ """
8
+ Ranks directories by absolute growth velocity (change) rather than sheer size.
9
+ """
10
+ def __init__(self, db: Database):
11
+ self.tracker = GrowthTracker(db)
12
+
13
+ def attribute_cause(self, top_dirs: List[DirNode], days: int = 7) -> List[Dict]:
14
+ """
15
+ Takes the top N directories, queries their historical growth,
16
+ and sorts them by who is growing the fastest.
17
+ """
18
+ results = []
19
+ for d in top_dirs:
20
+ rate = self.tracker.get_growth_rate(d.path, days=days)
21
+ velocity = rate.bytes_per_day if rate else 0.0
22
+
23
+ # Simple trend string
24
+ trend_str = "Stable"
25
+ if velocity > 1024 * 1024 * 50: # >50MB/day
26
+ trend_str = "Spiking ↑"
27
+ elif velocity > 1024 * 1024 * 5: # >5MB/day
28
+ trend_str = "Growing ↗"
29
+ elif velocity < -1024 * 1024:
30
+ trend_str = "Shrinking ↘"
31
+
32
+ results.append({
33
+ "path": d.path,
34
+ "current_size": d.size_bytes,
35
+ "velocity_per_day": velocity,
36
+ "trend": trend_str
37
+ })
38
+
39
+ # Sort by fastest growing first
40
+ results.sort(key=lambda x: x["velocity_per_day"], reverse=True)
41
+ return results