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:
Jan-Otto Kröpke
2026-10-09 01:48:17 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 301eef493b
commit 64046fb91f
9 changed files with 241 additions and 41 deletions
+79 -1
View File
@@ -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,
@@ -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",
@@ -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"})
}