Repository navigation
Description
Context
DockerStatsFetcher._get_network_stats() in
glances/plugins/containers/engines/docker.py reads network counters from a
single hardcoded interface:
eth0_stats = self._streamer.stats.get('networks', {}).get('eth0')The Docker stats endpoint returns one entry per container interface. A container
attached to several Docker networks (docker network connect, or several
networks declared in a Compose file) gets eth0, eth1, … and everything past
eth0 is silently dropped. Reported RX/TX rates for those containers are too
low.
Verified on both develop and develop-v5: the file is byte-identical on
the two branches, so the same patch applies to both.
Scope
In scope: _get_network_stats() in
glances/plugins/containers/engines/docker.py, plus unit tests.
Out of scope — do not touch:
glances/plugins/containers/engines/podman.py. Podman exposes only a
pre-aggregatedNetIOstring; there is no per-interface data to sum and no
bug to fix there.- The containers plugin model, the TUI, or the WebUI. The output contract of
_get_network_stats()is unchanged: same keys, same types, same units. - Any adjacent code, comment or formatting in
docker.py(CLAUDE.md §3).
Non-regression requirements
These must all hold after the change:
- Single-interface containers (the majority) produce identical values to
the current implementation. - Containers started with
--network hosthave nonetworkskey in the stats
payload. The method must keep returningNone, exactly as today. - The returned dict keeps the same shape:
cumulative_rx,cumulative_tx, and
— only when previous stats exist —time_since_update,rx,tx. - No new dependency, no new configuration key.
Plan
1. Patch `_get_network_stats()` to aggregate across interfaces
→ verify: `make lint && make format` clean; the diff touches only the
interface-selection and summation lines of that one method
2. Add `tests/test_plugin_containers.py` covering single-interface,
multi-interface, host-network, malformed-payload and rate-computation cases
→ verify: `uv run pytest tests/test_plugin_containers.py` passes
3. Run the existing suite to confirm no collateral damage
→ verify: `make test` shows no new failures versus the pre-change baseline
4. Add a NEWS.rst entry
→ verify: entry follows the existing `.rst` bullet format under the
unreleased sectionStep 1 — the patch
Replace the interface selection and the stats dict construction. Keep the rest
of the method (rate computation from _old_computed_stats) untouched.
interfaces_stats = self._streamer.stats.get('networks') or {}
# Checks for net_stats & mandatory fields
# Note: a container can be attached to several networks (eth0, eth1...)
# so all the interfaces should be aggregated (issue #XXXX)
valid_interfaces = [
interface_stats
for interface_name, interface_stats in interfaces_stats.items()
if interface_name != 'lo' and all(field in interface_stats for field in ['rx_bytes', 'tx_bytes'])
]
if not valid_interfaces:
self._log_debug("Missing Network usage fields")
return None
# Read the rx/tx stats (in bytes), summed over all the container interfaces
stats = {
'cumulative_rx': sum(interface_stats['rx_bytes'] for interface_stats in valid_interfaces),
'cumulative_tx': sum(interface_stats['tx_bytes'] for interface_stats in valid_interfaces),
}Replace #XXXX with the real issue number before committing.
Two deliberate choices, do not silently change them:
lois excluded. The Docker daemon does not normally report loopback in
networks, but Docker-API-compatible runtimes (nerdctl/containerd) have been
observed to. Loopback traffic would massively inflate the totals, so the guard
is cheap insurance rather than defensive noise.- Interfaces with missing counters are skipped, not fatal. Previously a
malformedeth0aborted the whole method. Now one malformed interface among
several no longer discards the valid ones.Noneis still returned when
nothing usable remains, which preserves requirement 2 above.
Decision required before implementing — do not decide alone
Summing across interfaces introduces a case that did not exist before: if a
container is attached to or detached from a network between two samples, the
interface set changes and cumulative_rx/cumulative_tx jump or drop. A drop
produces a negative rx/tx delta for one cycle.
Note that negative deltas are already possible today on container restart
(counters reset), so this is an aggravation of an existing behaviour rather than
a new class of bug.
Options:
- (a) Leave as is. Strictly surgical; one bogus sample in a rare scenario.
- (b) Clamp to zero:
max(0, stats['cumulative_rx'] - old[...]). Hides the
glitch, but also hides genuine counter resets and changes existing behaviour
for the restart case.
Default: (a). Do not implement (b) without explicit approval.
Step 2 — tests
Create tests/test_plugin_containers.py.
Style note: tests/test_plugin_sensors.py is written in pytest style
(fixtures, plain classes, bare assert) while older files such as
tests/test_core.py use unittest.TestCase. Match the sibling
test_plugin_*.py convention, i.e. pytest style, per CLAUDE.md §3.
DockerStatsFetcher.__init__ opens a live stats stream, so do not call it.
Build the instance without the constructor and inject the two attributes the
method reads:
from types import SimpleNamespace
from glances.plugins.containers.engines.docker import DockerStatsFetcher
def build_fetcher(networks, old_stats=None):
fetcher = DockerStatsFetcher.__new__(DockerStatsFetcher)
fetcher._container = SimpleNamespace(id='deadbeef')
fetcher._streamer = SimpleNamespace(
stats={'networks': networks} if networks is not None else {},
last_update_time=100.0,
)
fetcher._old_computed_stats = old_stats or {}
fetcher._last_stats_computed_time = 97.0
return fetcherCases to cover:
| Case | Input | Expected |
|---|---|---|
| Single interface | {'eth0': {'rx_bytes': 100, 'tx_bytes': 200}} |
cumulative_rx == 100, cumulative_tx == 200 (regression guard) |
| Two interfaces | eth0 100/200 + eth1 50/25 |
cumulative_rx == 150, cumulative_tx == 225 (the actual fix) |
| Loopback present | eth0 100/200 + lo 999/999 |
lo excluded, cumulative_rx == 100 |
| Host network | networks key absent |
returns None |
| Empty networks dict | {} |
returns None |
| All interfaces malformed | {'eth0': {'rx_bytes': 100}} |
returns None |
| One malformed among two | eth0 valid + eth1 missing tx_bytes |
eth1 skipped, eth0 still counted |
| Rate computation | two interfaces + _old_computed_stats['network'] with lower cumulative values |
rx/tx are the summed deltas, time_since_update present |
| First sample | no _old_computed_stats |
no rx/tx/time_since_update keys |
Step 3 — regression run
Capture a baseline before touching anything:
make test 2>&1 | tail -30 # baseline
# ... apply the change ...
make test 2>&1 | tail -30 # compareOnly the new test file may add results. Any other delta is a regression, stop
and report it.
Step 4 — changelog
Add to NEWS.rst, under the unreleased section, bug-fix list:
* Container network stats now aggregate all the container interfaces
instead of eth0 only (issue #XXXX)Branches
Apply to develop first. develop-v5 currently holds an identical copy of the
file, but re-verify the divergence before assuming a clean cherry-pick —
confirm the method body still matches, then port.
Report back
- Diff of
docker.py(should be roughly 10 lines). - Test file, and its pass output.
- Baseline vs post-change
make testcomparison. - Your recommendation on the decision point above, with reasoning.
Activity
fix(containers): aggregate network stats over all container interfaces