Merge branch 'develop' into develop-v5

This commit is contained in:
nicolargo
2026-05-17 11:26:07 +02:00
7 changed files with 111 additions and 5 deletions

View File

@@ -470,9 +470,9 @@ def auto_unit(number, low_precision=False, min_symbol='K', none_symbol='-'):
value = float(number) / prefix[symbol]
if value > 1:
decimal_precision = 0
if value < 10:
if value <= 9.995:
decimal_precision = 2
elif value < 100:
elif value < 99.95:
decimal_precision = 1
if low_precision:
if symbol in 'MK':

View File

@@ -52,6 +52,10 @@ fields_description = {
'description': 'Container CPU consumption',
'unit': 'percent',
},
'cpu_limit': {
'description': 'Container CPU limit',
'unit': 'number',
},
'memory_inactive_file': {
'description': 'Container memory inactive file',
'unit': 'byte',

View File

@@ -86,9 +86,9 @@ class DockerStatsFetcher:
def _get_cpu_stats(self) -> dict[str, float] | None:
"""Return the container CPU usage.
Output: a dict {'total': 1.49}
Output: a dict {'total': 1.49, 'limit': 2.0}
"""
stats = {'total': 0.0}
stats = {'total': 0.0, 'limit': 0.0}
try:
cpu_stats = self._streamer.stats['cpu_stats']
@@ -114,6 +114,18 @@ class DockerStatsFetcher:
self._log_debug("Can't compute CPU usage", e)
return None
host_config = self._container.attrs.get('HostConfig', {})
nano_cpus = host_config.get('NanoCpus', 0)
if nano_cpus > 0:
stats['limit'] = nano_cpus / 1e9
else:
cpu_quota = host_config.get('CpuQuota', 0)
cpu_period = host_config.get('CpuPeriod', 100_000)
if cpu_quota > 0 and cpu_period > 0:
stats['limit'] = cpu_quota / cpu_period
else:
stats['limit'] = float(cpu.get('nb_core', 0))
# Return the stats
return stats
@@ -342,6 +354,7 @@ class DockerExtension:
# Additional fields
stats['cpu_percent'] = stats['cpu']['total']
stats['cpu_limit'] = stats['cpu'].get('limit')
stats['memory_usage'] = stats['memory'].get('usage')
if stats['memory'].get('cache') is not None:
stats['memory_usage'] -= stats['memory']['cache']

View File

@@ -12,6 +12,8 @@ import time
from datetime import datetime
from typing import Any
import psutil
from glances.globals import nativestr, pretty_date
from glances.logger import logger
@@ -107,6 +109,16 @@ class LxdStatsFetcher:
stats["total"] = 0.0
except (KeyError, TypeError, ZeroDivisionError) as e:
logger.debug(f"containers (LXD) Instance({self._instance.name}): Can't grab CPU stats ({e})")
try:
limit = self._instance.expanded_config.get('limits.cpu')
if limit:
stats['limit'] = float(limit)
else:
stats['limit'] = float(psutil.cpu_count())
except (ValueError, TypeError):
stats['limit'] = float(psutil.cpu_count())
return stats
def _get_memory_stats(self, state) -> dict[str, Any]:
@@ -305,6 +317,7 @@ class LxdExtension:
# Additional fields
stats['cpu_percent'] = stats['cpu'].get('total')
stats['cpu_limit'] = stats['cpu'].get('limit')
stats['memory_usage'] = stats['memory'].get('usage')
stats['memory_inactive_file'] = stats['memory'].get('inactive_file')
stats['memory_limit'] = stats['memory'].get('limit')

View File

@@ -11,6 +11,8 @@ import time
from datetime import datetime
from typing import Any
import psutil
from glances.globals import nativestr, pretty_date, replace_special_chars, string_value_to_float
from glances.logger import logger
from glances.stats_streamer import ThreadedIterableStreamer
@@ -33,6 +35,12 @@ class PodmanContainerStatsFetcher:
def __init__(self, container):
self._container = container
# Full inspection to get limits (see issue #1152: List API doesn't include HostConfig)
try:
self._inspect_data = self._container.inspect()
except Exception:
self._inspect_data = {}
# Previous stats are stored in the self._old_computed_stats variable
# We store time data to enable rate calculations to avoid complexity for consumers of the APIs exposed.
self._old_computed_stats = {}
@@ -85,6 +93,19 @@ class PodmanContainerStatsFetcher:
try:
stats["cpu"]["total"] = api_stats["CPU"]
# CPU limit
host_config = self._inspect_data.get('HostConfig')
nano_cpus = host_config.get('NanoCpus', 0)
if nano_cpus > 0:
stats['cpu']['limit'] = nano_cpus / 1e9
else:
cpu_quota = host_config.get('CpuQuota', 0)
cpu_period = host_config.get('CpuPeriod', 0)
if cpu_quota > 0 and cpu_period > 0:
stats['cpu']['limit'] = cpu_quota / cpu_period
else:
# Fallback to the number of available cores
stats['cpu']['limit'] = float(psutil.cpu_count())
stats["memory"]["usage"] = api_stats["MemUsage"]
stats["memory"]["limit"] = api_stats["MemLimit"]
@@ -384,6 +405,7 @@ class PodmanExtension:
# Additional fields
stats["cpu_percent"] = stats["cpu"].get("total")
stats["cpu_limit"] = stats["cpu"].get("limit")
stats["memory_usage"] = stats["memory"].get("usage")
if stats["memory"].get("cache") is not None:
stats["memory_usage"] -= stats["memory"]["cache"]

View File

@@ -779,6 +779,30 @@ class TestGlances(unittest.TestCase):
self.assertEqual(auto_unit(1073741824, low_precision=True), '1024M')
self.assertEqual(auto_unit(1181116006), '1.10G')
self.assertEqual(auto_unit(1181116006, low_precision=True), '1.1G')
# Edge cases:
m = 2**20
g = 2**30
self.assertEqual(auto_unit(9.9 * m), '9.90M')
self.assertEqual(auto_unit(9.99 * m), '9.99M')
self.assertEqual(auto_unit(9.991 * m), '9.99M')
self.assertEqual(auto_unit(9.994 * m), '9.99M')
self.assertEqual(auto_unit(9.995 * m), '9.99M')
self.assertEqual(auto_unit(10 * m), '10.0M')
self.assertEqual(auto_unit(99 * m), '99.0M')
self.assertEqual(auto_unit(99.9 * m), '99.9M')
self.assertEqual(auto_unit(99.91 * m), '99.9M')
self.assertEqual(auto_unit(99.94 * m), '99.9M')
self.assertEqual(auto_unit(100 * m), '100M')
# Special cases that used to overflow and misalign columns
self.assertEqual(auto_unit(9.996 * m), '10.0M') # was 10.00M
self.assertEqual(auto_unit(9.999 * m), '10.0M') # was 10.00M
self.assertEqual(auto_unit(99.95 * m), '100M') # was 100.0M
self.assertEqual(auto_unit(99.96 * m), '100M') # was 100.0M
self.assertEqual(auto_unit(99.99 * m), '100M') # was 100.0M
self.assertEqual(auto_unit(10 * m - 1), '10.0M') # was 10.00M
self.assertEqual(auto_unit(100 * m - 1), '100M') # was 100.0M
self.assertEqual(auto_unit(10 * g - 1), '10.0G') # was 10.00G
self.assertEqual(auto_unit(100 * g - 1), '100G') # was 100.0G
def test_094_thresholds(self):
"""Test thresholds classes"""

View File

@@ -47,6 +47,7 @@ def make_mock_instance(
name="test-container",
status="Running",
config=None,
expanded_config=None,
expanded_devices=None,
created_at="2026-01-01T00:00:00Z",
last_used_at="2026-03-15T10:00:00Z",
@@ -57,6 +58,7 @@ def make_mock_instance(
instance.name = name
instance.status = status
instance.config = config or {"image.description": "Ubuntu 24.04 LTS"}
instance.expanded_config = expanded_config or {}
instance.expanded_devices = expanded_devices or {}
instance.created_at = created_at
instance.last_used_at = last_used_at
@@ -154,6 +156,33 @@ class TestLxdStatsFetcher:
# total=0 means unlimited, should fall back to usage_peak
assert stats["memory"]["limit"] == 4_000_000_000
def test_cpu_limit_explicit(self):
from glances.plugins.containers.engines.lxd import LxdStatsFetcher
instance = make_mock_instance(expanded_config={'limits.cpu': '4'})
instance.state = lambda: make_mock_state()
fetcher = LxdStatsFetcher(instance, poll_interval=999)
time.sleep(0.2)
stats = fetcher.activity_stats
fetcher.stop()
assert stats["cpu"]["limit"] == 4.0
def test_cpu_limit_fallback(self):
from glances.plugins.containers.engines.lxd import LxdStatsFetcher
instance = make_mock_instance(expanded_config={})
instance.state = lambda: make_mock_state()
with patch('psutil.cpu_count', return_value=8):
fetcher = LxdStatsFetcher(instance, poll_interval=999)
time.sleep(0.2)
stats = fetcher.activity_stats
fetcher.stop()
assert stats["cpu"]["limit"] == 8.0
def test_stop_terminates_thread(self, mock_instance):
from glances.plugins.containers.engines.lxd import LxdStatsFetcher
@@ -193,7 +222,7 @@ class TestLxdExtensionGenerateStats:
fetcher = MagicMock()
fetcher.activity_stats = {
"cpu": {"total": 50.0},
"cpu": {"total": 50.0, "limit": 2.0},
"memory": {"usage": 1_000_000_000, "limit": 2_000_000_000, "inactive_file": 0},
"io": {"ior": 100, "iow": 50, "time_since_update": 2},
"network": {"rx": 5000, "tx": 3000, "time_since_update": 2},
@@ -203,6 +232,7 @@ class TestLxdExtensionGenerateStats:
stats = ext.generate_stats(instance)
assert stats['cpu_percent'] == 50.0
assert stats['cpu_limit'] == 2.0
assert stats['memory_usage'] == 1_000_000_000
assert stats['memory_limit'] == 2_000_000_000
assert stats['io_rx'] == 50