Repository navigation
[BUG] Gauge dockerupdateguard.runtime_containers counts removed containers that still have snapshots #294
Description
Summary
The inventory gauge dockerupdateguard.runtime_containers counts every distinct container (per Docker instance and container ID) that has any stored snapshot. Snapshots of containers that have since been removed or recreated stay in the database until cleanup deletes them (default 30 days), so the gauge drifts far above the number of containers the UI shows and that actually exist. It should count only the currently known containers — the same set the Runtime Containers page shows, based on the latest runtime scan.
Area
docs/areas/telemetry.md(added in [Docs] Describe the current behavior by area #263) — Metrics, inventory gauges, Open question 2docs/areas/docker-instances.md,docs/areas/web-ui.md(definition of currently known containers; informational)- Security area: None
Current behavior
src/DockerUpdateGuard/ApplicationTelemetry.cs:142-149 (inside RefreshInventoryMetricsAsync, :135-171):
_runtimeContainers = await dbContext.ContainerSnapshots
.GroupBy(entity => new { entity.DockerInstanceId, entity.ContainerId })
.LongCountAsync(cancellationToken)
.ConfigureAwait(false);The gauge is registered at :94 and refreshed at startup, after every observed-image scan, runtime-container scan, vulnerability refresh and cleanup pass.
The UI defines "currently known containers" differently — src/DockerUpdateGuard/UI/ApplicationViewService.cs:1083-… (GetLatestContainerSnapshotsAsync): per Docker instance, the snapshots of the most recent RuntimeContainer scan run that has snapshots; for an instance without any runtime scan, the latest snapshot of each container. The live-image set uses the same rule (src/DockerUpdateGuard.Data/Queries/LiveImageInventoryQueryService.cs:41-82). Container discovery lists all containers including stopped ones (Docker/DockerInstanceClient.cs:996, containers/json?all=1), so a stopped container that still exists remains "currently known"; a removed one does not.
Snapshots older than RetainScanRunsDays are removed only by cleanup and only when no active update finding or container action refers to them (Images/ScanCleanupBackgroundService.cs:231-236).
Scenario: one Docker instance runs 10 containers; Compose recreates them on each deploy (new container IDs), twice a week. After 30 days the Runtime Containers page and the dashboard show 10 containers, while the gauge reports about 10 × (1 + 8) ≈ 90. An alert on "container count dropped/grew" is meaningless.
Expected behavior
dockerupdateguard.runtime_containersequals the number of currently known runtime containers: per Docker instance, the containers of the latest runtime container scan (the same scan the Runtime Containers page uses); for an instance that has no runtime scan yet, the latest snapshot per container.- The gauge value equals the runtime container count shown on the dashboard and the Runtime Containers page for the same database state.
- Refresh points and semantics of the other gauges stay unchanged.
Acceptance criteria
- With snapshots of an older runtime scan (containers A, B, C) and a newer runtime scan (containers A, D) for one instance, the gauge reports 2.
- With two instances, one with runtime scans and one with only non-runtime snapshots, the gauge combines both rules exactly like
GetLatestContainerSnapshotsAsync. - With no snapshots, the gauge reports 0.
- Unit tests (new
ApplicationTelemetryTests.csinsrc/Tests/DockerUpdateGuard.Tests, EF Core InMemory), e.g.ApplicationTelemetryRefreshInventoryMetricsCountsOnlyLatestRuntimeScanContainers,ApplicationTelemetryRefreshInventoryMetricsInstanceWithoutRuntimeScanUsesLatestSnapshotPerContainer,ApplicationTelemetryRefreshInventoryMetricsNoSnapshotsReportsZero. Read the gauge with aMeterListener(or an internal accessor) — assertions with messages. - A test proves the gauge and the UI count agree for the same data (shared query, see below).
- Coverage gate passes.
- The area document
docs/areas/telemetry.mddescribes the new gauge definition and its Known gaps entry for this issue is removed.
Implementation notes
- Recommended: move the "currently known containers" rule into one shared query in the data layer, next to
LiveImageInventoryQueryService(e.g. a method onILiveImageInventoryQueryServiceor a newIRuntimeContainerInventoryQueryServicereturning the current(DockerInstanceId, ContainerId)keys or their count), and use it fromApplicationTelemetry.RefreshInventoryMetricsAsync,ApplicationViewService.GetLatestContainerSnapshotsAsyncandLiveImageInventoryQueryService.GetRunningImageVersionIdsAsync, so the three definitions cannot drift apart again. RefreshInventoryMetricsAsynctakes theDbContextas a parameter and is called from several orchestrators and cleanup; keep that signature or pass the query service — both are acceptable, keep the call sites consistent.- Performance: the gauge is refreshed after every scan run. Prefer a query that only projects keys (no
Includes) instead of loading full snapshots; do not make F-026 (P3) —ResourceStatisticsCollectormaterializes the instance's entire sample history per cycle #80/F-027 (P3) —ScanCleanupBackgroundServiceloads all rows to be deleted into memory beforeRemoveRange#81 worse. - Must not change: metric name, unit (none), type (observable gauge), the fact that observing a gauge never queries the database, the refresh points.
- No configuration keys, no migration.
Related
- PR [Docs] Describe the current behavior by area #263 (area documents)
- F-026 (P3) —
ResourceStatisticsCollectormaterializes the instance's entire sample history per cycle #80 (resource collector loads history), F-027 (P3) —ScanCleanupBackgroundServiceloads all rows to be deleted into memory beforeRemoveRange#81 (cleanup loads all rows) — related query-cost concerns.
Generated by Claude Code
Activity