shield-stats 0.2.3__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.
- shield_stats-0.2.3/PKG-INFO +8 -0
- shield_stats-0.2.3/pyproject.toml +17 -0
- shield_stats-0.2.3/setup.cfg +4 -0
- shield_stats-0.2.3/shield_stats/__init__.py +3 -0
- shield_stats-0.2.3/shield_stats/cli.py +152 -0
- shield_stats-0.2.3/shield_stats/collector.py +99 -0
- shield_stats-0.2.3/shield_stats/storage.py +41 -0
- shield_stats-0.2.3/shield_stats.egg-info/PKG-INFO +8 -0
- shield_stats-0.2.3/shield_stats.egg-info/SOURCES.txt +10 -0
- shield_stats-0.2.3/shield_stats.egg-info/dependency_links.txt +1 -0
- shield_stats-0.2.3/shield_stats.egg-info/entry_points.txt +2 -0
- shield_stats-0.2.3/shield_stats.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shield-stats"
|
|
7
|
+
version = "0.2.3"
|
|
8
|
+
description = "Lightweight cross-distro Linux telemetry collector"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Reyansh Raj Mishra"}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
shield-stats = "shield_stats.cli:main"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from . import collector, storage
|
|
4
|
+
from .storage import load_snapshot_by_date
|
|
5
|
+
from datetime import date, timedelta
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _cmd_record() -> int:
|
|
10
|
+
metrics = collector.collect_metrics()
|
|
11
|
+
try:
|
|
12
|
+
storage.save_snapshot(metrics)
|
|
13
|
+
except Exception as e:
|
|
14
|
+
print(f"failed to save snapshot: {e}", file=sys.stderr)
|
|
15
|
+
return 1
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _format_bytes(n, signed=False):
|
|
20
|
+
value = float(n)
|
|
21
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
22
|
+
if abs(value) < 1024:
|
|
23
|
+
if signed:
|
|
24
|
+
return f"{value:+.2f} {unit}"
|
|
25
|
+
return f"{value:.2f} {unit}"
|
|
26
|
+
value /= 1024
|
|
27
|
+
if signed:
|
|
28
|
+
return f"{value:+.2f} PB"
|
|
29
|
+
return f"{value:.2f} PB"
|
|
30
|
+
|
|
31
|
+
def _cmd_summary() -> int:
|
|
32
|
+
today = date.today()
|
|
33
|
+
data = load_snapshot_by_date(today)
|
|
34
|
+
|
|
35
|
+
if not data:
|
|
36
|
+
print("No snapshot found for today.")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
print("System Snapshot (Today)\n")
|
|
40
|
+
|
|
41
|
+
# Disk (using df command for accurate calculation)
|
|
42
|
+
try:
|
|
43
|
+
df_output = subprocess.check_output(['df', '-h', '/'], text=True).splitlines()
|
|
44
|
+
disk_usage = df_output[1].split()
|
|
45
|
+
used = disk_usage[2] # Used space
|
|
46
|
+
total = disk_usage[1] # Total space
|
|
47
|
+
percent_disk = disk_usage[4] # Percentage used
|
|
48
|
+
print(f"Disk Usage: {used} of {total} ({percent_disk})")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Disk Usage: Error calculating disk usage: {e}")
|
|
51
|
+
|
|
52
|
+
# Memory
|
|
53
|
+
mem_total = data["memory_total_bytes"]
|
|
54
|
+
mem_used = data["memory_used_bytes"]
|
|
55
|
+
|
|
56
|
+
if mem_total and mem_used:
|
|
57
|
+
percent_mem = (mem_used / mem_total) * 100
|
|
58
|
+
print(f"Memory Usage: {_format_bytes(mem_used)} ({percent_mem:.1f}%)")
|
|
59
|
+
else:
|
|
60
|
+
print("Memory Usage: unavailable")
|
|
61
|
+
|
|
62
|
+
# Packages
|
|
63
|
+
print(f"Packages: {data['package_count']}")
|
|
64
|
+
|
|
65
|
+
# Load
|
|
66
|
+
print(f"Load (1m): {data['load_avg_1']:.2f}")
|
|
67
|
+
|
|
68
|
+
# Uptime
|
|
69
|
+
uptime = data["uptime_seconds"]
|
|
70
|
+
if uptime:
|
|
71
|
+
hours = int(uptime // 3600)
|
|
72
|
+
minutes = int((uptime % 3600) // 60)
|
|
73
|
+
print(f"Uptime: {hours}h {minutes}m")
|
|
74
|
+
|
|
75
|
+
# Kernel
|
|
76
|
+
print(f"Kernel: {data['kernel_version']}")
|
|
77
|
+
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
def _cmd_compare() -> int:
|
|
81
|
+
today = date.today()
|
|
82
|
+
yesterday = today - timedelta(days=1)
|
|
83
|
+
|
|
84
|
+
today_data = load_snapshot_by_date(today)
|
|
85
|
+
yesterday_data = load_snapshot_by_date(yesterday)
|
|
86
|
+
|
|
87
|
+
if not today_data:
|
|
88
|
+
print("No snapshot found for today.")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
if not yesterday_data:
|
|
92
|
+
print("No snapshot found for yesterday.")
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
print("Changes Since Yesterday:\n")
|
|
96
|
+
|
|
97
|
+
# Disk
|
|
98
|
+
disk_delta = today_data["disk_used_bytes"] - yesterday_data["disk_used_bytes"]
|
|
99
|
+
print(f"Disk Usage: {_format_bytes(disk_delta, signed=True)}")
|
|
100
|
+
|
|
101
|
+
# Memory
|
|
102
|
+
mem_delta = (
|
|
103
|
+
(today_data["memory_used_bytes"] or 0)
|
|
104
|
+
- (yesterday_data["memory_used_bytes"] or 0)
|
|
105
|
+
)
|
|
106
|
+
print(f"Memory Usage: {_format_bytes(mem_delta, signed=True)}")
|
|
107
|
+
|
|
108
|
+
# Packages
|
|
109
|
+
pkg_delta = today_data["package_count"] - yesterday_data["package_count"]
|
|
110
|
+
print(f"Packages: {pkg_delta:+}")
|
|
111
|
+
|
|
112
|
+
# Load
|
|
113
|
+
load_delta = (
|
|
114
|
+
(today_data["load_avg_1"] or 0)
|
|
115
|
+
- (yesterday_data["load_avg_1"] or 0)
|
|
116
|
+
)
|
|
117
|
+
print(f"Load (1m): {load_delta:+.2f}")
|
|
118
|
+
|
|
119
|
+
# Kernel change
|
|
120
|
+
if today_data["kernel_version"] != yesterday_data["kernel_version"]:
|
|
121
|
+
print(
|
|
122
|
+
f"Kernel: {yesterday_data['kernel_version']} → {today_data['kernel_version']}"
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
print("Kernel: unchanged")
|
|
126
|
+
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
def main(argv=None) -> int:
|
|
130
|
+
parser = argparse.ArgumentParser(prog="shield-stats")
|
|
131
|
+
sub = parser.add_subparsers(dest="command")
|
|
132
|
+
sub.required = True
|
|
133
|
+
|
|
134
|
+
sub.add_parser("record", help="collect and store today's metrics")
|
|
135
|
+
sub.add_parser("summary", help="show today's system summary")
|
|
136
|
+
sub.add_parser("compare", help="compare today with yesterday")
|
|
137
|
+
|
|
138
|
+
args = parser.parse_args(argv)
|
|
139
|
+
|
|
140
|
+
if args.command == "record":
|
|
141
|
+
return _cmd_record()
|
|
142
|
+
|
|
143
|
+
if args.command == "summary":
|
|
144
|
+
return _cmd_summary()
|
|
145
|
+
|
|
146
|
+
if args.command == "compare":
|
|
147
|
+
return _cmd_compare()
|
|
148
|
+
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Gather system metrics for shield-stats."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_proc_file(path: Path) -> str:
|
|
12
|
+
try:
|
|
13
|
+
with path.open("r") as f:
|
|
14
|
+
return f.read()
|
|
15
|
+
except Exception:
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_meminfo() -> Dict[str, int]:
|
|
20
|
+
data = _read_proc_file(Path("/proc/meminfo"))
|
|
21
|
+
result: Dict[str, int] = {}
|
|
22
|
+
for line in data.splitlines():
|
|
23
|
+
parts = line.split()
|
|
24
|
+
if len(parts) >= 2 and parts[0].endswith(":"):
|
|
25
|
+
key = parts[0].rstrip(":")
|
|
26
|
+
try:
|
|
27
|
+
value = int(parts[1]) * 1024
|
|
28
|
+
except ValueError:
|
|
29
|
+
continue
|
|
30
|
+
result[key] = value
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_uptime() -> Optional[float]:
|
|
35
|
+
data = _read_proc_file(Path("/proc/uptime")).split()
|
|
36
|
+
try:
|
|
37
|
+
return float(data[0])
|
|
38
|
+
except (IndexError, ValueError):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_load_avg() -> Optional[float]:
|
|
43
|
+
data = _read_proc_file(Path("/proc/loadavg")).split()
|
|
44
|
+
try:
|
|
45
|
+
return float(data[0])
|
|
46
|
+
except (IndexError, ValueError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _count_packages() -> int:
|
|
51
|
+
if shutil.which("pacman"):
|
|
52
|
+
cmd = ["pacman", "-Qq"]
|
|
53
|
+
|
|
54
|
+
elif shutil.which("apt"):
|
|
55
|
+
cmd = ["dpkg-query", "-f", "${binary:Package}\n", "-W"]
|
|
56
|
+
|
|
57
|
+
elif shutil.which("dnf"):
|
|
58
|
+
cmd = ["rpm", "-qa"]
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
65
|
+
except Exception:
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
if proc.returncode != 0:
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
return len(proc.stdout.strip().splitlines())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def collect_metrics() -> Dict[str, Optional[object]]:
|
|
75
|
+
now = int(time.time())
|
|
76
|
+
|
|
77
|
+
total, used, _free = shutil.disk_usage("/")
|
|
78
|
+
|
|
79
|
+
meminfo = _parse_meminfo()
|
|
80
|
+
mem_total = meminfo.get("MemTotal")
|
|
81
|
+
mem_available = meminfo.get("MemAvailable")
|
|
82
|
+
mem_used = (
|
|
83
|
+
mem_total - mem_available
|
|
84
|
+
if mem_total is not None and mem_available is not None
|
|
85
|
+
else None
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"timestamp": now,
|
|
90
|
+
"hostname": platform.node(),
|
|
91
|
+
"disk_total_bytes": total,
|
|
92
|
+
"disk_used_bytes": used,
|
|
93
|
+
"memory_total_bytes": mem_total,
|
|
94
|
+
"memory_used_bytes": mem_used,
|
|
95
|
+
"uptime_seconds": _get_uptime(),
|
|
96
|
+
"load_avg_1": _get_load_avg(),
|
|
97
|
+
"package_count": _count_packages(),
|
|
98
|
+
"kernel_version": platform.release(),
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Handle storage of collected metrics."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
from datetime import date
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_snapshot_by_date(d: date):
|
|
13
|
+
base = Path.home() / ".local" / "share" / "shield-stats"
|
|
14
|
+
path = base / f"{d.isoformat()}.json"
|
|
15
|
+
if not path.exists():
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
with path.open("r", encoding="utf-8") as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
except Exception:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
def _get_storage_path() -> Path:
|
|
24
|
+
base = Path.home() / ".local" / "share" / "shield-stats"
|
|
25
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
today = date.today().isoformat()
|
|
27
|
+
return base / f"{today}.json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save_snapshot(data: Dict[str, Any]) -> None:
|
|
31
|
+
path = _get_storage_path()
|
|
32
|
+
temp = path.with_suffix(".tmp")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
with temp.open("w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
37
|
+
f.flush()
|
|
38
|
+
temp.replace(path)
|
|
39
|
+
except Exception:
|
|
40
|
+
with path.open("w", encoding="utf-8") as f:
|
|
41
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
shield_stats/__init__.py
|
|
3
|
+
shield_stats/cli.py
|
|
4
|
+
shield_stats/collector.py
|
|
5
|
+
shield_stats/storage.py
|
|
6
|
+
shield_stats.egg-info/PKG-INFO
|
|
7
|
+
shield_stats.egg-info/SOURCES.txt
|
|
8
|
+
shield_stats.egg-info/dependency_links.txt
|
|
9
|
+
shield_stats.egg-info/entry_points.txt
|
|
10
|
+
shield_stats.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shield_stats
|