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.
- diskrx-0.1.0.dist-info/METADATA +234 -0
- diskrx-0.1.0.dist-info/RECORD +33 -0
- diskrx-0.1.0.dist-info/WHEEL +5 -0
- diskrx-0.1.0.dist-info/entry_points.txt +2 -0
- diskrx-0.1.0.dist-info/licenses/LICENSE +21 -0
- diskrx-0.1.0.dist-info/top_level.txt +1 -0
- dxcli/__init__.py +6 -0
- dxcli/analyzers/__init__.py +15 -0
- dxcli/analyzers/anomaly.py +58 -0
- dxcli/analyzers/correlation.py +32 -0
- dxcli/analyzers/growth.py +53 -0
- dxcli/analyzers/predictor.py +44 -0
- dxcli/analyzers/prescriptions.py +61 -0
- dxcli/analyzers/root_cause.py +41 -0
- dxcli/cli.py +249 -0
- dxcli/collectors/__init__.py +0 -0
- dxcli/collectors/dir_tree.py +56 -0
- dxcli/collectors/log_finder.py +45 -0
- dxcli/collectors/process_mapper.py +62 -0
- dxcli/collectors/stale_files.py +41 -0
- dxcli/config.py +15 -0
- dxcli/outputs/__init__.py +0 -0
- dxcli/outputs/cli_report.py +242 -0
- dxcli/outputs/metrics.py +42 -0
- dxcli/outputs/tui.py +286 -0
- dxcli/outputs/tui.tcss +78 -0
- dxcli/platform/__init__.py +13 -0
- dxcli/platform/base.py +9 -0
- dxcli/platform/linux.py +27 -0
- dxcli/platform/windows.py +25 -0
- dxcli/store/__init__.py +0 -0
- dxcli/store/database.py +112 -0
- dxcli/store/models.py +60 -0
|
@@ -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
|
+
[](https://pypi.org/project/dxcli/)
|
|
40
|
+
[](https://pypi.org/project/dxcli/)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[](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,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,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
|