From 64046fb91f55b4f2ac413ac7200c49c60e5ffbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 Oct 2026 01:48:17 +0200 Subject: [PATCH] container: add start time, process count and page fault metrics (#2566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 5.5 Signed-off-by: Jan-Otto Kröpke --- docs/collector.container.md | 47 ++++++----- internal/collector/container/container.go | 80 ++++++++++++++++++- .../container/container_internal_test.go | 4 +- .../collector/container/container_test.go | 17 ++++ internal/headers/cri/cri_internal_test.go | 8 +- internal/headers/cri/types.go | 8 ++ internal/headers/hcs/hcs.go | 29 ++++--- internal/headers/hcs/hcs_internal_test.go | 75 +++++++++++++++++ internal/headers/hcs/types.go | 14 ++-- 9 files changed, 241 insertions(+), 41 deletions(-) create mode 100644 internal/headers/hcs/hcs_internal_test.go diff --git a/docs/collector.container.md b/docs/collector.container.md index 37e58e6d..8079bd63 100644 --- a/docs/collector.container.md +++ b/docs/collector.container.md @@ -26,26 +26,33 @@ If the endpoint is not available, for example on hosts without Kubernetes, the H ## Metrics -| Name | Description | Type | Labels | -|------------------------------------------------------------|----------------------------------------|---------|------------------------------------------------------------| -| `windows_container_available` | Available | gauge | `container_id`,`namespace`,`pod`,`container`,`hostprocess` | -| `windows_container_count` | Number of running HCS containers | gauge | None | -| `windows_container_cpu_usage_seconds_kernelmode` | Runtime in Kernel mode in Seconds | counter | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_cpu_usage_seconds_usermode` | Runtime in User mode in Seconds | counter | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_cpu_usage_seconds_total` | Total Runtime in Seconds | counter | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_memory_usage_commit_bytes` | Memory Usage Commit Bytes | gauge | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_memory_usage_commit_peak_bytes` | Memory Usage Commit Peak Bytes | gauge | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_memory_usage_private_working_set_bytes` | Memory Usage Private Working Set Bytes | gauge | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_network_receive_bytes_total` | Bytes Received on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | -| `windows_container_network_receive_packets_total` | Packets Received on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | -| `windows_container_network_receive_packets_dropped_total` | Dropped Incoming Packets on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | -| `windows_container_network_transmit_bytes_total` | Bytes Sent on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | -| `windows_container_network_transmit_packets_total` | Packets Sent on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | -| `windows_container_network_transmit_packets_dropped_total` | Dropped Outgoing Packets on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | -| `windows_container_storage_read_count_normalized_total` | Read Count Normalized | counter | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_storage_read_size_bytes_total` | Read Size Bytes | counter | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_storage_write_count_normalized_total` | Write Count Normalized | counter | `container_id`,`namespace`,`pod`,`container` | -| `windows_container_storage_write_size_bytes_total` | Write Size Bytes | counter | `container_id`,`namespace`,`pod`,`container` | +| Name | Description | Type | Labels | +|------------------------------------------------------------|-----------------------------------------------------|---------|------------------------------------------------------------| +| `windows_container_available` | Available | gauge | `container_id`,`namespace`,`pod`,`container`,`hostprocess` | +| `windows_container_count` | Number of running HCS containers | gauge | None | +| `windows_container_cpu_usage_seconds_kernelmode` | Runtime in Kernel mode in Seconds | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_cpu_usage_seconds_usermode` | Runtime in User mode in Seconds | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_cpu_usage_seconds_total` | Total Runtime in Seconds | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_memory_page_faults_total` | Total number of page faults (HostProcess only) | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_memory_usage_commit_bytes` | Memory Usage Commit Bytes | gauge | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_memory_usage_commit_peak_bytes` | Memory Usage Commit Peak Bytes | gauge | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_memory_usage_private_working_set_bytes` | Memory Usage Private Working Set Bytes | gauge | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_network_receive_bytes_total` | Bytes Received on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | +| `windows_container_network_receive_packets_total` | Packets Received on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | +| `windows_container_network_receive_packets_dropped_total` | Dropped Incoming Packets on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | +| `windows_container_network_transmit_bytes_total` | Bytes Sent on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | +| `windows_container_network_transmit_packets_total` | Packets Sent on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | +| `windows_container_network_transmit_packets_dropped_total` | Dropped Outgoing Packets on Interface | counter | `container_id`,`namespace`,`pod`,`container`,`interface` | +| `windows_container_processes` | Number of processes running in the container | gauge | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_start_time_seconds` | Start time of the container since Unix epoch | gauge | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_storage_read_count_normalized_total` | Read Count Normalized | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_storage_read_size_bytes_total` | Read Size Bytes | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_storage_write_count_normalized_total` | Write Count Normalized | counter | `container_id`,`namespace`,`pod`,`container` | +| `windows_container_storage_write_size_bytes_total` | Write Size Bytes | counter | `container_id`,`namespace`,`pod`,`container` | + +`windows_container_start_time_seconds` is the start time reported by HCS. HostProcess containers have no start time, they report their creation time from the CRI endpoint instead. + +`windows_container_memory_page_faults_total` is only available for HostProcess containers, because HCS doesn't report page faults. The job object counts page faults in 32 bits, so the counter wraps around after 2^32 page faults. ### Example metric _windows_container_network_receive_bytes_total{container_id="docker://1bd30e8b8ac28cbd76a9b697b4d7bb9d760267b0733d1bc55c60024e98d1e43e",interface="822179E7-002C-4280-ABBA-28BCFE401826"} 9.3305343e+07_ diff --git a/internal/collector/container/container.go b/internal/collector/container/container.go index f3eeabcf..5e4a489a 100644 --- a/internal/collector/container/container.go +++ b/internal/collector/container/container.go @@ -94,10 +94,15 @@ type Collector struct { // Number of containers containersCount *prometheus.Desc + // Lifecycle + startTime *prometheus.Desc + processes *prometheus.Desc + // Memory usageCommitBytes *prometheus.Desc usageCommitPeakBytes *prometheus.Desc usagePrivateWorkingSetBytes *prometheus.Desc + pageFaults *prometheus.Desc // CPU runtimeTotal *prometheus.Desc @@ -124,6 +129,8 @@ type containerInfo struct { namespace string pod string container string + // createdAt is the creation time reported by the CRI endpoint. It is zero for containers not managed by Kubernetes. + createdAt time.Time } // kubernetesContainers holds the Kubernetes metadata of the running containers, read from the CRI endpoint. @@ -159,6 +166,7 @@ func newKubernetesContainers(runtimeName string, sandboxes []cri.PodSandbox, con namespace: pod.Namespace, pod: pod.Name, container: container.Name, + createdAt: container.CreatedAt, } } @@ -268,6 +276,18 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { nil, nil, ) + c.startTime = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "start_time_seconds"), + "Start time of the container since Unix epoch in seconds. HostProcess containers report their creation time.", + []string{"container_id", "namespace", "pod", "container"}, + nil, + ) + c.processes = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "processes"), + "Number of processes running in the container", + []string{"container_id", "namespace", "pod", "container"}, + nil, + ) c.usageCommitBytes = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "memory_usage_commit_bytes"), "Memory Usage Commit Bytes", @@ -286,6 +306,12 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { []string{"container_id", "namespace", "pod", "container"}, nil, ) + c.pageFaults = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "memory_page_faults_total"), + "Total number of page faults of the container processes. Only available for HostProcess containers.", + []string{"container_id", "namespace", "pod", "container"}, + nil, + ) c.runtimeTotal = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "cpu_usage_seconds_total"), "Total Run time in Seconds", @@ -547,11 +573,13 @@ func (c *Collector) collectHCS(ch chan<- prometheus.Metric, kubernetes kubernete } func (c *Collector) collectHCSContainer(ch chan<- prometheus.Metric, containerDetails hcs.Properties, containerInfo containerInfo) error { - containerStats, err := hcs.GetContainerStatistics(containerDetails.ID) + properties, err := hcs.GetContainerStatistics(containerDetails.ID) if err != nil { return fmt.Errorf("error fetching container statistics: %w", err) } + containerStats := properties.Statistics + ch <- prometheus.MustNewConstMetric( c.containerAvailable, prometheus.GaugeValue, @@ -559,6 +587,29 @@ func (c *Collector) collectHCSContainer(ch chan<- prometheus.Metric, containerDe containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, "false", ) + startTime := containerStats.ContainerStartTime + if startTime.IsZero() { + startTime = containerInfo.createdAt + } + + if !startTime.IsZero() { + ch <- prometheus.MustNewConstMetric( + c.startTime, + prometheus.GaugeValue, + float64(startTime.UnixNano())/1e9, + + containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, + ) + } + + ch <- prometheus.MustNewConstMetric( + c.processes, + prometheus.GaugeValue, + float64(len(properties.ProcessList)), + + containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, + ) + ch <- prometheus.MustNewConstMetric( c.usageCommitBytes, prometheus.GaugeValue, @@ -793,6 +844,33 @@ func (c *Collector) collectJobContainer(ch chan<- prometheus.Metric, containerID containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, "true", ) + // Job objects have no start time, so the creation time reported by the CRI endpoint is used. + if !containerInfo.createdAt.IsZero() { + ch <- prometheus.MustNewConstMetric( + c.startTime, + prometheus.GaugeValue, + float64(containerInfo.createdAt.UnixNano())/1e9, + + containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, + ) + } + + ch <- prometheus.MustNewConstMetric( + c.processes, + prometheus.GaugeValue, + float64(jobInfo.BasicInfo.ActiveProcesses), + + containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, + ) + + ch <- prometheus.MustNewConstMetric( + c.pageFaults, + prometheus.CounterValue, + float64(jobInfo.BasicInfo.TotalPageFaultCount), + + containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, + ) + ch <- prometheus.MustNewConstMetric( c.usageCommitBytes, prometheus.GaugeValue, diff --git a/internal/collector/container/container_internal_test.go b/internal/collector/container/container_internal_test.go index e79e04a1..fe4acba4 100644 --- a/internal/collector/container/container_internal_test.go +++ b/internal/collector/container/container_internal_test.go @@ -22,6 +22,7 @@ import ( "fmt" "log/slog" "testing" + "time" "github.com/prometheus-community/windows_exporter/internal/headers/cri" "github.com/prometheus/client_golang/prometheus" @@ -38,7 +39,7 @@ func TestNewKubernetesContainers(t *testing.T) { {ID: "sandbox-2", Name: "stopped", Namespace: "kube-system", State: cri.SandboxNotReady}, }, []cri.Container{ - {ID: "abc", PodSandboxID: "sandbox-1", Name: "nanoserver", State: cri.ContainerRunning}, + {ID: "abc", PodSandboxID: "sandbox-1", Name: "nanoserver", State: cri.ContainerRunning, CreatedAt: time.Unix(1700000000, 0)}, {ID: "orphan", PodSandboxID: "unknown", Name: "orphan", State: cri.ContainerRunning}, {ID: "exited", PodSandboxID: "sandbox-1", Name: "init", State: cri.ContainerExited}, }, @@ -50,6 +51,7 @@ func TestNewKubernetesContainers(t *testing.T) { namespace: "default", pod: "pod", container: "nanoserver", + createdAt: time.Unix(1700000000, 0), }, "orphan": { id: "containerd://orphan", diff --git a/internal/collector/container/container_test.go b/internal/collector/container/container_test.go index 471d8204..cdf206bf 100644 --- a/internal/collector/container/container_test.go +++ b/internal/collector/container/container_test.go @@ -19,10 +19,12 @@ package container_test import ( "testing" + "time" "github.com/prometheus-community/windows_exporter/internal/collector/container" "github.com/prometheus-community/windows_exporter/internal/utils/testutils" "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" ) func BenchmarkCollector(b *testing.B) { @@ -33,4 +35,19 @@ func TestCollector(t *testing.T) { metrics := testutils.TestCollector(t, container.New, nil) testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_available", prometheus.Labels{"container": "hostprocess", "namespace": "default", "hostprocess": "true"}) testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_available", prometheus.Labels{"container": "nanoserver", "namespace": "default", "hostprocess": "false"}) + + for _, name := range []string{"hostprocess", "nanoserver"} { + labels := prometheus.Labels{"container": name, "namespace": "default"} + + if metric := testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_processes", labels); metric != nil { + require.Positive(t, metric.GetGauge().GetValue(), "container %s has no processes", name) + } + + if metric := testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_start_time_seconds", labels); metric != nil { + require.InDelta(t, float64(time.Now().Unix()), metric.GetGauge().GetValue(), float64(24*time.Hour/time.Second), + "container %s start time is not within the last day", name) + } + } + + testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_memory_page_faults_total", prometheus.Labels{"container": "hostprocess", "namespace": "default"}) } diff --git a/internal/headers/cri/cri_internal_test.go b/internal/headers/cri/cri_internal_test.go index 00ca418f..bea647a7 100644 --- a/internal/headers/cri/cri_internal_test.go +++ b/internal/headers/cri/cri_internal_test.go @@ -23,6 +23,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/stretchr/testify/require" "golang.org/x/sys/windows" @@ -69,7 +70,7 @@ func containersResponse() message { message(4, message{}.string(1, "mcr.microsoft.com/windows/nanoserver")). string(5, "sha256:1234"). varint(6, uint64(ContainerRunning)). - varint(7, 1700000000000000000) + varint(7, 1700000000123456789) nanoserver = append(nanoserver, labels(8, "io.kubernetes.pod.name", "pod")...) nanoserver = append(nanoserver, labels(9, "io.kubernetes.container.restartCount", "2")...) @@ -100,9 +101,12 @@ func TestDecodeListContainersResponse(t *testing.T) { containers, err := decodeListContainersResponse(containersResponse()) require.NoError(t, err) require.Equal(t, []Container{ - {ID: "abc", PodSandboxID: "sandbox-1", Name: "nanoserver", State: ContainerRunning}, + {ID: "abc", PodSandboxID: "sandbox-1", Name: "nanoserver", State: ContainerRunning, CreatedAt: time.Unix(0, 1700000000123456789)}, {ID: "def", Name: "other", State: ContainerCreated}, }, containers) + + require.Equal(t, int64(1700000000123456789), containers[0].CreatedAt.UnixNano()) + require.True(t, containers[1].CreatedAt.IsZero()) } func TestDecodeListPodSandboxResponse(t *testing.T) { diff --git a/internal/headers/cri/types.go b/internal/headers/cri/types.go index 8b7d088f..b52e0d86 100644 --- a/internal/headers/cri/types.go +++ b/internal/headers/cri/types.go @@ -18,6 +18,8 @@ package cri import ( + "time" + "google.golang.org/protobuf/encoding/protowire" ) @@ -54,6 +56,8 @@ type Container struct { // Name is the container name from the Kubernetes pod spec. Name string State ContainerState + // CreatedAt is the creation time of the container. It is zero if the runtime did not set it. + CreatedAt time.Time } // PodSandbox is the subset of runtime.v1.PodSandbox used by the exporter. @@ -170,6 +174,10 @@ func decodeContainer(b []byte) (Container, error) { }) case f.is(6, protowire.VarintType): container.State = ContainerState(f.value) + case f.is(7, protowire.VarintType): // created_at, int64 in nanoseconds since the Unix epoch + if f.value != 0 { + container.CreatedAt = time.Unix(0, int64(f.value)) + } } return nil diff --git a/internal/headers/hcs/hcs.go b/internal/headers/hcs/hcs.go index b4d06624..beb26d5e 100644 --- a/internal/headers/hcs/hcs.go +++ b/internal/headers/hcs/hcs.go @@ -27,8 +27,9 @@ import ( //nolint:gochecknoglobals var ( - ContainerQuery = utils.Must(windows.UTF16PtrFromString(`{"Types":["Container"]}`)) - StatisticsQuery = utils.Must(windows.UTF16PtrFromString(`{"PropertyTypes":["Statistics"]}`)) + ContainerQuery = utils.Must(windows.UTF16PtrFromString(`{"Types":["Container"]}`)) + // StatisticsQuery queries the statistics and the process list in one call. + StatisticsQuery = utils.Must(windows.UTF16PtrFromString(`{"PropertyTypes":["Statistics","ProcessList"]}`)) ) func GetContainers() ([]Properties, error) { @@ -58,40 +59,46 @@ func GetContainers() ([]Properties, error) { return computeSystems, nil } -func GetContainerStatistics(containerID string) (Statistics, error) { +// GetContainerStatistics returns the statistics and the process list of a container. +// The Statistics field is never nil if no error is returned. +func GetContainerStatistics(containerID string) (Properties, error) { computeSystem, err := OpenComputeSystem(containerID) if err != nil { - return Statistics{}, fmt.Errorf("failed to open compute system: %w", err) + return Properties{}, fmt.Errorf("failed to open compute system: %w", err) } defer CloseComputeSystem(computeSystem) operation, err := CreateOperation() if err != nil { - return Statistics{}, fmt.Errorf("failed to create operation: %w", err) + return Properties{}, fmt.Errorf("failed to create operation: %w", err) } defer CloseOperation(operation) if err := GetComputeSystemProperties(computeSystem, operation, StatisticsQuery); err != nil { - return Statistics{}, fmt.Errorf("failed to enumerate compute systems: %w", err) + return Properties{}, fmt.Errorf("failed to enumerate compute systems: %w", err) } resultDocument, err := WaitForOperationResult(operation, 1000) if err != nil { - return Statistics{}, fmt.Errorf("failed to get compute system properties: %w", err) + return Properties{}, fmt.Errorf("failed to get compute system properties: %w", err) } else if resultDocument == "" { - return Statistics{}, ErrEmptyResultDocument + return Properties{}, ErrEmptyResultDocument } + return parseStatistics(containerID, resultDocument) +} + +func parseStatistics(containerID, resultDocument string) (Properties, error) { var properties Properties if err := json.Unmarshal([]byte(resultDocument), &properties); err != nil { - return Statistics{}, fmt.Errorf("failed to unmarshal system properties: %w", err) + return Properties{}, fmt.Errorf("failed to unmarshal system properties: %w", err) } if properties.Statistics == nil { - return Statistics{}, fmt.Errorf("no statistics found for container %s", containerID) + return Properties{}, fmt.Errorf("no statistics found for container %s", containerID) } - return *properties.Statistics, nil + return properties, nil } diff --git a/internal/headers/hcs/hcs_internal_test.go b/internal/headers/hcs/hcs_internal_test.go new file mode 100644 index 00000000..0bf78b2c --- /dev/null +++ b/internal/headers/hcs/hcs_internal_test.go @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package hcs + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestParseStatistics(t *testing.T) { + t.Parallel() + + // A result document of the Statistics and ProcessList query, as HCS returns it for a process-isolated container. + properties, err := parseStatistics("abc", `{ + "Id": "abc", + "SystemType": "Container", + "Statistics": { + "Timestamp": "2026-10-08T10:00:05.1234567Z", + "ContainerStartTime": "2026-10-08T10:00:00.5Z", + "Uptime100ns": 50000000, + "Processor": {"TotalRuntime100ns": 30000000, "RuntimeUser100ns": 20000000, "RuntimeKernel100ns": 10000000}, + "Memory": {"MemoryUsageCommitBytes": 1, "MemoryUsageCommitPeakBytes": 2, "MemoryUsagePrivateWorkingSetBytes": 3}, + "Storage": {"ReadCountNormalized": 4, "ReadSizeBytes": 5, "WriteCountNormalized": 6, "WriteSizeBytes": 7} + }, + "ProcessList": [ + {"CreateTimestamp": "2026-10-08T10:00:00.6Z", "ImageName": "smss.exe", "ProcessId": 4096}, + { + "CreateTimestamp": "2026-10-08T10:00:01Z", + "ImageName": "app.exe", + "KernelTime100ns": 9000000000, + "MemoryCommitBytes": 8589934592, + "MemoryWorkingSetPrivateBytes": 4294967296, + "MemoryWorkingSetSharedBytes": 4096, + "ProcessId": 4294967292, + "UserTime100ns": 9000000000 + } + ] +}`) + require.NoError(t, err) + require.NotNil(t, properties.Statistics) + require.Equal(t, time.Date(2026, 10, 8, 10, 0, 0, 500_000_000, time.UTC), properties.Statistics.ContainerStartTime) + require.Equal(t, uint64(30000000), properties.Statistics.Processor.TotalRuntime100ns) + require.Equal(t, uint64(7), properties.Statistics.Storage.WriteSizeBytes) + require.Len(t, properties.ProcessList, 2) + require.Equal(t, uint32(4294967292), properties.ProcessList[1].ProcessId) + require.Equal(t, uint64(8589934592), properties.ProcessList[1].MemoryCommitBytes) + require.Equal(t, uint64(9000000000), properties.ProcessList[1].UserTime100ns) +} + +func TestParseStatisticsMissing(t *testing.T) { + t.Parallel() + + _, err := parseStatistics("abc", `{"Id": "abc", "ProcessList": []}`) + require.ErrorContains(t, err, "no statistics found for container abc") + + _, err = parseStatistics("abc", `{"Statistics": `) + require.Error(t, err) +} diff --git a/internal/headers/hcs/types.go b/internal/headers/hcs/types.go index fcdcc82c..91ba1a9a 100644 --- a/internal/headers/hcs/types.go +++ b/internal/headers/hcs/types.go @@ -48,15 +48,17 @@ type Properties struct { ProcessList []ProcessDetails `json:"ProcessList,omitempty"` } +// ProcessDetails is an entry of the ProcessList property, see ProcessListItem in hcsshim's schema1. +// The times and sizes are 64-bit, e.g. a process using more than 2 GiB of memory would overflow an int32. type ProcessDetails struct { - ProcessId int32 `json:"ProcessId,omitempty"` + ProcessId uint32 `json:"ProcessId,omitempty"` ImageName string `json:"ImageName,omitempty"` CreateTimestamp time.Time `json:"CreateTimestamp"` - UserTime100ns int32 `json:"UserTime100ns,omitempty"` - KernelTime100ns int32 `json:"KernelTime100ns,omitempty"` - MemoryCommitBytes int32 `json:"MemoryCommitBytes,omitempty"` - MemoryWorkingSetPrivateBytes int32 `json:"MemoryWorkingSetPrivateBytes,omitempty"` - MemoryWorkingSetSharedBytes int32 `json:"MemoryWorkingSetSharedBytes,omitempty"` + UserTime100ns uint64 `json:"UserTime100ns,omitempty"` + KernelTime100ns uint64 `json:"KernelTime100ns,omitempty"` + MemoryCommitBytes uint64 `json:"MemoryCommitBytes,omitempty"` + MemoryWorkingSetPrivateBytes uint64 `json:"MemoryWorkingSetPrivateBytes,omitempty"` + MemoryWorkingSetSharedBytes uint64 `json:"MemoryWorkingSetSharedBytes,omitempty"` } type Statistics struct {