Skip to content

Fix container network accounting: sum all interfaces, not only eth0 #3669

Closed

Description

@nicolargo

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-aggregated NetIO string; 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:

  1. Single-interface containers (the majority) produce identical values to
    the current implementation.
  2. Containers started with --network host have no networks key in the stats
    payload. The method must keep returning None, exactly as today.
  3. The returned dict keeps the same shape: cumulative_rx, cumulative_tx, and
    — only when previous stats exist — time_since_update, rx, tx.
  4. 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 section

Step 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:

  • lo is 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
    malformed eth0 aborted the whole method. Now one malformed interface among
    several no longer discards the valid ones. None is 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 fetcher

Cases 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   # compare

Only 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 test comparison.
  • Your recommendation on the decision point above, with reasoning.

Activity

  1. added this to the Glances 4.5.7 milestone on Aug 18, 2026
  2. added a commit that references this issue on Aug 22, 2026
    061402b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions