mirror of
https://github.com/nicolargo/glances.git
synced 2026-06-02 19:05:00 -04:00
Merge branch 'develop' into develop-v5
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user