Skip to content

Commit acf4d90

Browse files
committed
Feature: extend health memory reporting
- add host total free and available memory to /health - keep process RSS and computed usage percent in the memory block - update health pytest coverage for the expanded response shape - update endpoint and Kubernetes docs for the new memory fields - 2026-03-19 02:46:59
1 parent 3efc0c7 commit acf4d90

6 files changed

Lines changed: 103 additions & 5 deletions

File tree

docs/api/fast-api/pypnm/system/health.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ Use this endpoint for:
2929
"uptime": 65
3030
},
3131
"memory": {
32-
"rss_bytes": 12582912
32+
"rss_bytes": 12582912,
33+
"total_bytes": 17179869184,
34+
"free_bytes": 8216707072,
35+
"available_bytes": 10379091968,
36+
"usage_percent": 0.07
3337
},
3438
"data": {
3539
"path": ".data",
@@ -58,13 +62,21 @@ Use this endpoint for:
5862
| `uptime.starttime` | integer | Service start time as Unix epoch seconds. |
5963
| `uptime.uptime` | integer | Elapsed uptime in whole seconds since `starttime`. |
6064
| `memory.rss_bytes` | integer | Current resident memory used by the running PyPNM process, in bytes. |
65+
| `memory.total_bytes` | integer | Total system memory on the current host, in bytes. |
66+
| `memory.free_bytes` | integer | Free system memory on the current host, in bytes. |
67+
| `memory.available_bytes` | integer | Available system memory on the current host, in bytes. |
68+
| `memory.usage_percent` | number | Resident process memory as a percent of total system memory. |
6169
| `data.path` | string | Runtime data root path. |
6270
| `data.size_bytes` | integer | Recursive apparent size of the `.data` directory in bytes. |
6371
| `data.directories` | object | Recursive apparent sizes for each first-level directory under `.data`. |
6472

6573
## Notes
6674

6775
- `memory.rss_bytes` is based on Linux resident set size (`VmRSS`).
76+
- `memory.total_bytes` is based on Linux total memory (`MemTotal`).
77+
- `memory.free_bytes` is based on Linux free memory (`MemFree`).
78+
- `memory.available_bytes` is based on Linux available memory (`MemAvailable`).
79+
- `memory.usage_percent` is computed as `rss_bytes / total_bytes * 100`.
6880
- `data.size_bytes` is logical file size, not filesystem block allocation.
6981
- `data.directories` helps identify which artifact classes are consuming disk.
7082
- This endpoint is intended to stay lightweight and local to the running service.

docs/kubernetes/kind-freelens.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ Example response:
5252
"uptime": 1
5353
},
5454
"memory": {
55-
"rss_bytes": 12582912
55+
"rss_bytes": 12582912,
56+
"total_bytes": 17179869184,
57+
"free_bytes": 8216707072,
58+
"available_bytes": 10379091968,
59+
"usage_percent": 0.07
5660
},
5761
"data": {
5862
"path": ".data",
@@ -72,6 +76,10 @@ Example response:
7276
```
7377

7478
`memory.rss_bytes` reports the current resident memory used by the running PyPNM process in bytes.
79+
`memory.total_bytes` reports host total memory in bytes.
80+
`memory.free_bytes` reports host free memory in bytes.
81+
`memory.available_bytes` reports host available memory in bytes.
82+
`memory.usage_percent` reports resident process memory as a percent of host total memory.
7583

7684
If the returned `service.version` is older than expected, verify the tag used in the deploy command and confirm the namespace matches the running deployment.
7785

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66

77
[project]
88
name = "pypnm-docsis"
9-
version = "1.5.8.0"
9+
version = "1.5.8.1"
1010
description = "DOCSIS 3.x/4.0 Proactive Network Maintenance Toolkit"
1111
readme = "README.md"
1212
requires-python = ">=3.10"

src/pypnm/api/main.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ class HealthDataModel(BaseModel):
9090

9191
class HealthMemoryModel(BaseModel):
9292
rss_bytes: int = Field(..., description="Resident memory usage for the running PyPNM process in bytes.")
93+
total_bytes: int = Field(..., description="Total system memory available on the current host in bytes.")
94+
free_bytes: int = Field(..., description="Free system memory on the current host in bytes.")
95+
available_bytes: int = Field(..., description="Available system memory on the current host in bytes.")
96+
usage_percent: float = Field(..., description="Resident process memory as a percentage of total system memory.")
9397

9498

9599
class HealthResponseModel(BaseModel):
@@ -192,6 +196,63 @@ def _process_rss_bytes() -> int:
192196
return 0
193197

194198

199+
def _system_total_memory_bytes() -> int:
200+
"""Return total system memory in bytes using Linux procfs."""
201+
meminfo_path = pathlib.Path("/proc/meminfo")
202+
if not meminfo_path.is_file():
203+
return 0
204+
205+
try:
206+
for line in meminfo_path.read_text(encoding="utf-8").splitlines():
207+
if not line.startswith("MemTotal:"):
208+
continue
209+
parts = line.split()
210+
if len(parts) < 2:
211+
return 0
212+
return int(parts[1]) * 1024
213+
except (OSError, ValueError):
214+
return 0
215+
216+
return 0
217+
218+
219+
def _read_meminfo_bytes(field_name: str) -> int:
220+
"""Return a specific `/proc/meminfo` field in bytes."""
221+
meminfo_path = pathlib.Path("/proc/meminfo")
222+
if not meminfo_path.is_file():
223+
return 0
224+
225+
try:
226+
for line in meminfo_path.read_text(encoding="utf-8").splitlines():
227+
if not line.startswith(f"{field_name}:"):
228+
continue
229+
parts = line.split()
230+
if len(parts) < 2:
231+
return 0
232+
return int(parts[1]) * 1024
233+
except (OSError, ValueError):
234+
return 0
235+
236+
return 0
237+
238+
239+
def _system_free_memory_bytes() -> int:
240+
"""Return free system memory in bytes using Linux procfs."""
241+
return _read_meminfo_bytes("MemFree")
242+
243+
244+
def _system_available_memory_bytes() -> int:
245+
"""Return available system memory in bytes using Linux procfs."""
246+
return _read_meminfo_bytes("MemAvailable")
247+
248+
249+
def _process_memory_usage_percent(rss_bytes: int, total_bytes: int) -> float:
250+
"""Return process RSS as a percentage of total system memory."""
251+
if total_bytes <= 0:
252+
return 0.0
253+
return round((rss_bytes / total_bytes) * 100.0, 2)
254+
255+
195256
def _apply_muted_tag_policy(app_instance: FastAPI) -> None:
196257
_hard_muted_routes.clear()
197258
muted_tags = read_env_csv_set(ENV_MUTE_TAGS)
@@ -214,11 +275,21 @@ def health() -> HealthResponseModel:
214275
"""Lightweight health endpoint for probes."""
215276
uptime_seconds = _service_uptime_seconds()
216277
data_root = _data_root_path()
278+
rss_bytes = _process_rss_bytes()
279+
total_memory_bytes = _system_total_memory_bytes()
280+
free_memory_bytes = _system_free_memory_bytes()
281+
available_memory_bytes = _system_available_memory_bytes()
217282
return HealthResponseModel(
218283
status="ok",
219284
service=HealthServiceModel(name=SERVICE_NAME, version=__version__),
220285
uptime=HealthUptimeModel(starttime=API_START_EPOCH, uptime=uptime_seconds),
221-
memory=HealthMemoryModel(rss_bytes=_process_rss_bytes()),
286+
memory=HealthMemoryModel(
287+
rss_bytes=rss_bytes,
288+
total_bytes=total_memory_bytes,
289+
free_bytes=free_memory_bytes,
290+
available_bytes=available_memory_bytes,
291+
usage_percent=_process_memory_usage_percent(rss_bytes, total_memory_bytes),
292+
),
222293
data=HealthDataModel(
223294
path=str(data_root),
224295
size_bytes=_folder_size_bytes(data_root),

src/pypnm/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
__all__ = ["__version__"]
77

88
# MAJOR.MINOR.MAINTENANCE.BUILD
9-
__version__: str = "1.5.8.0"
9+
__version__: str = "1.5.8.1"

tests/test_api_health.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def test_health_includes_uptime(monkeypatch) -> None:
1515
monkeypatch.setattr(api_main, "SERVICE_NAME", "pypnm-docsis")
1616
monkeypatch.setattr(api_main, "_data_root_path", lambda: Path(".data"))
1717
monkeypatch.setattr(api_main, "_process_rss_bytes", lambda: 12_582_912)
18+
monkeypatch.setattr(api_main, "_system_total_memory_bytes", lambda: 17_179_869_184)
19+
monkeypatch.setattr(api_main, "_system_free_memory_bytes", lambda: 8_216_707_072)
20+
monkeypatch.setattr(api_main, "_system_available_memory_bytes", lambda: 10_379_091_968)
1821
monkeypatch.setattr(api_main, "_folder_size_bytes", lambda _: 4096)
1922
monkeypatch.setattr(api_main, "_first_level_directory_sizes", lambda _: {"pnm": 1024, "json": 2048})
2023
monkeypatch.setattr(api_main, "monotonic", lambda: 165.9)
@@ -33,6 +36,10 @@ def test_health_includes_uptime(monkeypatch) -> None:
3336
},
3437
"memory": {
3538
"rss_bytes": 12_582_912,
39+
"total_bytes": 17_179_869_184,
40+
"free_bytes": 8_216_707_072,
41+
"available_bytes": 10_379_091_968,
42+
"usage_percent": 0.07,
3643
},
3744
"data": {
3845
"path": ".data",

0 commit comments

Comments
 (0)