mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
container: collect Hyper-V isolated containers from CRI stats (#2568)
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
200f248eba
commit
fedca642a1
@@ -121,6 +121,16 @@ func (c *Client) ListPodSandboxes(ctx context.Context) ([]PodSandbox, error) {
|
||||
return decodeListPodSandboxResponse(resp)
|
||||
}
|
||||
|
||||
// ListContainerStats returns the stats of all containers.
|
||||
func (c *Client) ListContainerStats(ctx context.Context) ([]ContainerStats, error) {
|
||||
resp, err := c.call(ctx, "ListContainerStats", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decodeListContainerStatsResponse(resp)
|
||||
}
|
||||
|
||||
// call performs a unary gRPC call and returns the response message.
|
||||
func (c *Client) call(ctx context.Context, method string, msg []byte) ([]byte, error) {
|
||||
// A gRPC message is prefixed by a compression flag and its length.
|
||||
|
||||
@@ -95,6 +95,63 @@ func sandboxesResponse() message {
|
||||
varint(3, uint64(SandboxNotReady)))
|
||||
}
|
||||
|
||||
func uint64Value(v uint64) message {
|
||||
return message{}.varint(1, v)
|
||||
}
|
||||
|
||||
// A ListContainerStatsResponse as containerd sends it on Windows, including fields the client skips.
|
||||
func containerStatsResponse() message {
|
||||
attributes := message{}.
|
||||
string(1, "abc").
|
||||
message(2, message{}.string(1, "nanoserver"))
|
||||
attributes = append(attributes, labels(3, "io.kubernetes.pod.name", "pod")...)
|
||||
|
||||
running := message{}.
|
||||
message(1, attributes).
|
||||
message(2, message{}.
|
||||
varint(1, 1700000000000000000).
|
||||
message(2, uint64Value(1500000000)).
|
||||
message(3, uint64Value(20000000))).
|
||||
message(3, message{}.
|
||||
varint(1, 1700000000000000000).
|
||||
message(2, uint64Value(4096)).
|
||||
message(4, uint64Value(8192))).
|
||||
message(4, message{}.
|
||||
varint(1, 1700000000000000001).
|
||||
message(2, message{}.string(1, `C:\ProgramData\containerd\root`)).
|
||||
message(3, uint64Value(1024)).
|
||||
message(4, uint64Value(0)))
|
||||
|
||||
// A container without task metrics, e.g. a created container, only has attributes and a
|
||||
// writable layer which containerd has not measured yet. The value 0 is sent as empty message.
|
||||
created := message{}.
|
||||
message(1, message{}.string(1, "def")).
|
||||
message(4, message{}.message(3, message{}))
|
||||
|
||||
return message{}.message(1, running).message(1, created)
|
||||
}
|
||||
|
||||
func TestDecodeListContainerStatsResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ptr := func(v uint64) *uint64 { return &v }
|
||||
|
||||
stats, err := decodeListContainerStatsResponse(containerStatsResponse())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ContainerStats{
|
||||
{
|
||||
ID: "abc",
|
||||
CPU: &CPUUsage{Timestamp: 1700000000000000000, UsageCoreNanoSeconds: ptr(1500000000)},
|
||||
Memory: &MemoryUsage{Timestamp: 1700000000000000000, WorkingSetBytes: ptr(4096), UsageBytes: ptr(8192)},
|
||||
WritableLayer: &FilesystemUsage{Timestamp: 1700000000000000001, UsedBytes: ptr(1024)},
|
||||
},
|
||||
{
|
||||
ID: "def",
|
||||
WritableLayer: &FilesystemUsage{UsedBytes: ptr(0)},
|
||||
},
|
||||
}, stats)
|
||||
}
|
||||
|
||||
func TestDecodeListContainersResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -133,6 +190,10 @@ func TestDecodeInvalidMessage(t *testing.T) {
|
||||
// A nested message with an invalid length.
|
||||
_, err = decodeListPodSandboxResponse(message{}.message(1, message{0x12, 0x7f}))
|
||||
require.Error(t, err)
|
||||
|
||||
// A UInt64Value with an invalid length.
|
||||
_, err = decodeListContainerStatsResponse(message{}.message(1, message{}.message(2, message{}.message(2, message{0x08}))))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncodeListRunningContainersRequest(t *testing.T) {
|
||||
@@ -200,6 +261,7 @@ func TestClient(t *testing.T) {
|
||||
message{}.string(1, "0.1.0").string(2, "containerd").string(3, "v2.1.4").string(4, "v1")))
|
||||
mux.Handle(runtimeService+"ListContainers", grpcHandler(t, encodeListRunningContainersRequest(), containersResponse()))
|
||||
mux.Handle(runtimeService+"ListPodSandbox", grpcHandler(t, nil, sandboxesResponse()))
|
||||
mux.Handle(runtimeService+"ListContainerStats", grpcHandler(t, nil, containerStatsResponse()))
|
||||
|
||||
client, err := NewClient(startServer(t, mux))
|
||||
require.NoError(t, err)
|
||||
@@ -221,6 +283,13 @@ func TestClient(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sandboxes, 2)
|
||||
require.Equal(t, "default", sandboxes[0].Namespace)
|
||||
|
||||
stats, err := client.ListContainerStats(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 2)
|
||||
require.Equal(t, "abc", stats[0].ID)
|
||||
require.NotNil(t, stats[0].CPU)
|
||||
require.Equal(t, uint64(1500000000), *stats[0].CPU.UsageCoreNanoSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,45 @@ type PodSandbox struct {
|
||||
State PodSandboxState
|
||||
}
|
||||
|
||||
// ContainerStats is the subset of runtime.v1.ContainerStats used by the exporter.
|
||||
// Nil fields were not reported by the runtime.
|
||||
//
|
||||
// On Windows, containerd fills them from the HCS statistics of the container,
|
||||
// which the shim also reads for Hyper-V isolated containers.
|
||||
// https://github.com/containerd/containerd/blob/v2.4.1/internal/cri/server/container_stats_list.go
|
||||
type ContainerStats struct {
|
||||
ID string
|
||||
CPU *CPUUsage
|
||||
Memory *MemoryUsage
|
||||
WritableLayer *FilesystemUsage
|
||||
}
|
||||
|
||||
// CPUUsage is the subset of runtime.v1.CpuUsage used by the exporter.
|
||||
type CPUUsage struct {
|
||||
// Timestamp is the time of the sample in nanoseconds since the Unix epoch.
|
||||
Timestamp int64
|
||||
// UsageCoreNanoSeconds is the cumulative CPU time of the container.
|
||||
UsageCoreNanoSeconds *uint64
|
||||
}
|
||||
|
||||
// MemoryUsage is the subset of runtime.v1.MemoryUsage used by the exporter.
|
||||
type MemoryUsage struct {
|
||||
// Timestamp is the time of the sample in nanoseconds since the Unix epoch.
|
||||
Timestamp int64
|
||||
// WorkingSetBytes is the private working set on Windows.
|
||||
WorkingSetBytes *uint64
|
||||
// UsageBytes is the commit size on Windows.
|
||||
UsageBytes *uint64
|
||||
}
|
||||
|
||||
// FilesystemUsage is the subset of runtime.v1.FilesystemUsage used by the exporter.
|
||||
type FilesystemUsage struct {
|
||||
// Timestamp is the time of the sample in nanoseconds since the Unix epoch.
|
||||
// containerd sends 0 if it has not measured the usage yet.
|
||||
Timestamp int64
|
||||
UsedBytes *uint64
|
||||
}
|
||||
|
||||
// field is a decoded protobuf field. value holds varints, bytes length-delimited values.
|
||||
type field struct {
|
||||
num protowire.Number
|
||||
@@ -237,6 +276,112 @@ func decodePodSandbox(b []byte) (PodSandbox, error) {
|
||||
return sandbox, err
|
||||
}
|
||||
|
||||
func decodeListContainerStatsResponse(b []byte) ([]ContainerStats, error) {
|
||||
var stats []ContainerStats
|
||||
|
||||
err := eachField(b, func(f field) error {
|
||||
if !f.is(1, protowire.BytesType) {
|
||||
return nil
|
||||
}
|
||||
|
||||
s, err := decodeContainerStats(f.bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stats = append(stats, s)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func decodeContainerStats(b []byte) (ContainerStats, error) {
|
||||
var stats ContainerStats
|
||||
|
||||
err := eachField(b, func(f field) error {
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case f.is(1, protowire.BytesType): // ContainerAttributes
|
||||
err = eachField(f.bytes, func(f field) error {
|
||||
if f.is(1, protowire.BytesType) {
|
||||
stats.ID = string(f.bytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
case f.is(2, protowire.BytesType):
|
||||
stats.CPU = &CPUUsage{}
|
||||
err = eachField(f.bytes, func(f field) error {
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case f.is(1, protowire.VarintType):
|
||||
stats.CPU.Timestamp = int64(f.value)
|
||||
case f.is(2, protowire.BytesType):
|
||||
stats.CPU.UsageCoreNanoSeconds, err = decodeUInt64Value(f.bytes)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
case f.is(3, protowire.BytesType):
|
||||
stats.Memory = &MemoryUsage{}
|
||||
err = eachField(f.bytes, func(f field) error {
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case f.is(1, protowire.VarintType):
|
||||
stats.Memory.Timestamp = int64(f.value)
|
||||
case f.is(2, protowire.BytesType):
|
||||
stats.Memory.WorkingSetBytes, err = decodeUInt64Value(f.bytes)
|
||||
case f.is(4, protowire.BytesType):
|
||||
stats.Memory.UsageBytes, err = decodeUInt64Value(f.bytes)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
case f.is(4, protowire.BytesType):
|
||||
stats.WritableLayer = &FilesystemUsage{}
|
||||
err = eachField(f.bytes, func(f field) error {
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case f.is(1, protowire.VarintType):
|
||||
stats.WritableLayer.Timestamp = int64(f.value)
|
||||
case f.is(3, protowire.BytesType):
|
||||
stats.WritableLayer.UsedBytes, err = decodeUInt64Value(f.bytes)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// decodeUInt64Value decodes a runtime.v1.UInt64Value. An empty message is the value 0.
|
||||
func decodeUInt64Value(b []byte) (*uint64, error) {
|
||||
var value uint64
|
||||
|
||||
err := eachField(b, func(f field) error {
|
||||
if f.is(1, protowire.VarintType) {
|
||||
value = f.value
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
// encodeListRunningContainersRequest encodes a ListContainersRequest
|
||||
// with filter.state.state set to CONTAINER_RUNNING.
|
||||
func encodeListRunningContainersRequest() []byte {
|
||||
|
||||
Reference in New Issue
Block a user