feat(docker): uptime from StartedAt, a restart is an event, host totals count physical devices only (#5153)

- Agent: started_at comes from inspect's State.StartedAt (nanoseconds
  trimmed; Docker's zero time is "never started"), not the list's Created,
  so a restarted container's uptime counts from its restart.
- A rising restart_count between two snapshots emits a "restart" event
  ("restart #3", or "3 restarts, now #5"), said instead of a start; a
  restart then a death gives both. A count that falls (recreated under the
  same name) says nothing. The timeline draws it as ↻.
- Network and disk I/O count what the kernel calls physical: interfaces
  not under /sys/devices/virtual/net (bridges, docker0, veth, vxlan, tun),
  block devices with a device behind them (not dm, md, zram), so a byte is
  not counted twice. Where /sys calls everything virtual (an agent in LXC)
  the name-filtered set is kept rather than reporting no traffic.
- Agent tests updated for #5152's (status, detail, containers) and
  (is_manager, topology) shapes; the no-Docker disk test now states the
  case it was passing for by accident.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 18:45:57 -04:00
co-authored by Claude Opus 5.5
parent 8ceb983183
commit bcd0563f39
8 changed files with 170 additions and 32 deletions
+54 -2
View File
@@ -54,7 +54,9 @@ def test_rates_skips_names_missing_from_prev():
assert agent._rates({"new": (5, 5)}, {}, dt=1.0) == {}
def test_collect_net_raw_skips_lo_and_veth(monkeypatch):
def test_collect_net_raw_skips_lo_and_veth(monkeypatch, tmp_path):
# /sys can't say (no such tree): the name filter alone decides.
monkeypatch.setattr(agent, "SYS_VIRTUAL_NET_DIR", str(tmp_path / "absent"))
fixture = (
"Inter-| Receive\n"
" face |bytes\n"
@@ -66,7 +68,8 @@ def test_collect_net_raw_skips_lo_and_veth(monkeypatch):
assert agent.collect_net_raw() == {"eth0": (1000, 500)}
def test_collect_diskio_raw_whole_disks_only(monkeypatch):
def test_collect_diskio_raw_whole_disks_only(monkeypatch, tmp_path):
monkeypatch.setattr(agent, "SYS_BLOCK_DIR", str(tmp_path / "absent"))
# fields: major minor name reads rd_merged sectors_read ms ... sectors_written ...
fixture = (
" 8 0 sda 1 0 10 0 1 0 20 0 0 0 0\n"
@@ -79,6 +82,55 @@ def test_collect_diskio_raw_whole_disks_only(monkeypatch):
assert out == {"sda": (10 * 512, 20 * 512), "nvme0n1": (100 * 512, 200 * 512)}
_NETDEV = (
"Inter-| Receive\n"
" face |bytes\n"
" lo: 100 1 0 0 0 0 0 0 200 2 0 0 0 0 0 0\n"
" eno1: 1000 5 0 0 0 0 0 0 500 4 0 0 0 0 0 0\n"
"docker0: 70 1 0 0 0 0 0 0 80 1 0 0 0 0 0 0\n"
"br-1a2b: 30 1 0 0 0 0 0 0 40 1 0 0 0 0 0 0\n"
"vxlan0: 10 1 0 0 0 0 0 0 20 1 0 0 0 0 0 0\n"
)
def _sys_net(tmp_path, virtual):
d = tmp_path / "virtual-net"
for name in virtual:
(d / name).mkdir(parents=True)
return str(d)
def test_collect_net_raw_counts_physical_interfaces_only(monkeypatch, tmp_path):
# Bridge, docker0 and vxlan traffic also crosses eno1: counting them too
# would count those bytes twice in the host total (#5153).
monkeypatch.setattr(agent, "SYS_VIRTUAL_NET_DIR",
_sys_net(tmp_path, ["lo", "docker0", "br-1a2b", "vxlan0"]))
monkeypatch.setattr(agent, "_read_file", lambda p: _NETDEV)
assert agent.collect_net_raw() == {"eno1": (1000, 500)}
def test_collect_net_raw_keeps_everything_when_sys_calls_it_all_virtual(monkeypatch, tmp_path):
# Inside LXC eth0 is a veth peer: all virtual. No traffic at all would be wrong.
netdev = "h\nh\n eth0: 1000 5 0 0 0 0 0 0 500 4 0 0 0 0 0 0\n"
monkeypatch.setattr(agent, "SYS_VIRTUAL_NET_DIR", _sys_net(tmp_path, ["lo", "eth0"]))
monkeypatch.setattr(agent, "_read_file", lambda p: netdev)
assert agent.collect_net_raw() == {"eth0": (1000, 500)}
def test_collect_diskio_raw_counts_physical_disks_only(monkeypatch, tmp_path):
# dm-0 (LVM on sda) and md0 (RAID over sdb, sdc) repeat their members' I/O.
block = tmp_path / "block"
for name in ("sda", "sdb", "nvme0n1"):
(block / name / "device").mkdir(parents=True)
for name in ("dm-0", "md0", "zram0"):
(block / name).mkdir(parents=True)
diskstats = "".join(f" 8 0 {n} 1 0 10 0 1 0 20 0 0 0 0\n"
for n in ("sda", "sdb", "nvme0n1", "dm-0", "md0", "zram0"))
monkeypatch.setattr(agent, "SYS_BLOCK_DIR", str(block))
monkeypatch.setattr(agent, "_read_file", lambda p: diskstats)
assert sorted(agent.collect_diskio_raw()) == ["nvme0n1", "sda", "sdb"]
def test_build_sample_first_call_has_no_rates_but_keeps_state(monkeypatch):
monkeypatch.setattr(agent, "collect_cpu", lambda: (5.0, [5.0]))
monkeypatch.setattr(agent, "collect_memory", lambda: {"total_bytes": 10, "available_bytes": 5})