mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
container: add start time, process count and page fault metrics (#2566)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
301eef493b
commit
64046fb91f
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
-11
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user