diff --git a/cpu_v2.go b/cpu_v2.go index 1f505380f64d587ab14af5d9038c88b9f34f5544..39d4d30582bb3c9d6a9ae5ebee0e5b73704ded58 100644 --- a/cpu_v2.go +++ b/cpu_v2.go @@ -6,12 +6,29 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var usecs float64 = 1000000 +var ( + usecs float64 = 1000000 + + cpuV2PressureStalledDesc = prometheus.NewDesc( + "cgroup_cpu_pressure_stalled_seconds_total", + "PSI stalled CPU seconds.", + []string{"slice", "service"}, + nil, + ) + cpuV2PressureWaitingDesc = prometheus.NewDesc( + "cgroup_cpu_pressure_waiting_seconds_total", + "PSI waiting CPU seconds.", + []string{"slice", "service"}, + nil, + ) +) type cpuV2Parser struct{} func (p *cpuV2Parser) describe(ch chan<- *prometheus.Desc) { ch <- cpuV1Desc + ch <- cpuV2PressureStalledDesc + ch <- cpuV2PressureWaitingDesc } func (p *cpuV2Parser) parse(path, slice, unit string, ch chan<- prometheus.Metric) { @@ -33,4 +50,20 @@ func (p *cpuV2Parser) parse(path, slice, unit string, ch chan<- prometheus.Metri float64(usage["system_usec"])/usecs, "system", slice, unit, ) + + waiting, stalled, err := parsePressureFile(filepath.Join(cgroupsRootPath, path, "cpu.pressure")) + if err == nil { + ch <- prometheus.MustNewConstMetric( + cpuV2PressureWaitingDesc, + prometheus.CounterValue, + float64(waiting), + slice, unit, + ) + ch <- prometheus.MustNewConstMetric( + cpuV2PressureStalledDesc, + prometheus.CounterValue, + float64(stalled), + slice, unit, + ) + } } diff --git a/io_v2.go b/io_v2.go index 2203be1ca5697c0ed4436670b415e2aff899b2e2..a7a0e7089aa27c4b3602dd95f6299b88062e1e1a 100644 --- a/io_v2.go +++ b/io_v2.go @@ -24,6 +24,18 @@ var ( []string{"mode", "slice", "service"}, nil, ) + ioV2PressureStalledDesc = prometheus.NewDesc( + "cgroup_blkio_pressure_stalled_seconds_total", + "PSI stalled I/O seconds.", + []string{"slice", "service"}, + nil, + ) + ioV2PressureWaitingDesc = prometheus.NewDesc( + "cgroup_blkio_pressure_waiting_seconds_total", + "PSI waiting I/O seconds.", + []string{"slice", "service"}, + nil, + ) ) type blkioV2Parser struct{} @@ -44,17 +56,23 @@ func parseDevice(token []byte) (int, int, error) { return maj, min, nil } +func parseKVPair(token []byte) (string, int64, error) { + kvp := bytes.SplitN(token, []byte("="), 2) + if len(kvp) != 2 { + return "", 0, errors.New("not an assignment") + } + value, err := strconv.ParseInt(string(kvp[1]), 10, 64) + if err != nil { + return "", 0, err + } + return string(kvp[0]), value, nil +} + func parseKVPairs(tokens [][]byte, out map[string]int64) { for _, token := range tokens { - kvp := bytes.SplitN(token, []byte("="), 2) - if len(kvp) != 2 { - continue + if key, value, err := parseKVPair(token); err == nil { + out[key] += value } - value, err := strconv.ParseInt(string(kvp[1]), 10, 64) - if err != nil { - continue - } - out[string(kvp[0])] += value } } @@ -65,9 +83,8 @@ func parseIOV2File(path string) (map[string]int64, error) { } defer f.Close() - // Sum I/O counters across devices, but only for mounted - // devices, so we do not count I/O operations twice with - // LVM/MD. + // Sum I/O counters across devices, but only for mounted devices, + // so we do not count I/O operations twice with LVM/MD. result := make(map[string]int64) scanner := bufio.NewScanner(f) for scanner.Scan() { @@ -92,6 +109,8 @@ func parseIOV2File(path string) (map[string]int64, error) { func (p *blkioV2Parser) describe(ch chan<- *prometheus.Desc) { ch <- ioV2BytesDesc ch <- ioV2OpsDesc + ch <- ioV2PressureStalledDesc + ch <- ioV2PressureWaitingDesc } func (p *blkioV2Parser) parse(path, slice, unit string, ch chan<- prometheus.Metric) { @@ -124,4 +143,20 @@ func (p *blkioV2Parser) parse(path, slice, unit string, ch chan<- prometheus.Met float64(counters["rios"]), "read", slice, unit, ) + + waiting, stalled, err := parsePressureFile(filepath.Join(cgroupsRootPath, path, "io.pressure")) + if err == nil { + ch <- prometheus.MustNewConstMetric( + ioV2PressureWaitingDesc, + prometheus.CounterValue, + float64(waiting), + slice, unit, + ) + ch <- prometheus.MustNewConstMetric( + ioV2PressureStalledDesc, + prometheus.CounterValue, + float64(stalled), + slice, unit, + ) + } } diff --git a/mem_v2.go b/mem_v2.go index 6549d3ef0dcb0602b793f2c3d65cae0ef57bfc0f..838cdac4e9934762d7492b8db21ac8a72e303d62 100644 --- a/mem_v2.go +++ b/mem_v2.go @@ -6,10 +6,27 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +var ( + memV2PressureStalledDesc = prometheus.NewDesc( + "cgroup_mem_pressure_stalled_seconds_total", + "PSI stalled memory seconds.", + []string{"slice", "service"}, + nil, + ) + memV2PressureWaitingDesc = prometheus.NewDesc( + "cgroup_mem_pressure_waiting_seconds_total", + "PSI waiting memory seconds.", + []string{"slice", "service"}, + nil, + ) +) + type memoryV2Parser struct{} func (p *memoryV2Parser) describe(ch chan<- *prometheus.Desc) { ch <- memV1Desc + ch <- memV2PressureStalledDesc + ch <- memV2PressureWaitingDesc } func (p *memoryV2Parser) parse(path, slice, unit string, ch chan<- prometheus.Metric) { @@ -25,4 +42,20 @@ func (p *memoryV2Parser) parse(path, slice, unit string, ch chan<- prometheus.Me float64(rss), slice, unit, ) + + waiting, stalled, err := parsePressureFile(filepath.Join(cgroupsRootPath, path, "memory.pressure")) + if err == nil { + ch <- prometheus.MustNewConstMetric( + memV2PressureWaitingDesc, + prometheus.CounterValue, + float64(waiting), + slice, unit, + ) + ch <- prometheus.MustNewConstMetric( + memV2PressureStalledDesc, + prometheus.CounterValue, + float64(stalled), + slice, unit, + ) + } } diff --git a/util.go b/util.go index f0999be696d055cd5ecbb9c312c690f30e3623d7..3a667f80e45b84e49279fe5e50758ffd41264c96 100644 --- a/util.go +++ b/util.go @@ -17,6 +17,8 @@ var ( ) func init() { + // Cgroups v1 counters are expressed in 'ticks', so we need to figure + // out the system's HZ value to convert them to seconds. userHZ = 100 if clktck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK); err == nil { userHZ = float64(clktck) @@ -27,6 +29,8 @@ func cgroupV1StatPath(cgroupPath, collector, path string) string { return filepath.Join("/sys/fs/cgroup", collector, cgroupPath, path) } +// Parse a generic proc-style 'map' file, with space-separated "key value" +// assignments, one per line. func parseMapFile(path string) (map[string]int64, error) { f, err := os.Open(path) if err != nil { @@ -51,6 +55,7 @@ func parseMapFile(path string) (map[string]int64, error) { return result, scanner.Err() } +// Parse a file containing a single integer value. func parseSingleValueFile(path string) (int64, error) { data, err := ioutil.ReadFile(path) if err != nil { @@ -62,6 +67,37 @@ func parseSingleValueFile(path string) (int64, error) { return strconv.ParseInt(string(data), 10, 64) } +// Parse a PSI /proc file and return the "some" (waiting), "full" (stalled) +// counters. +func parsePressureFile(path string) (int64, int64, error) { + f, err := os.Open(path) + if err != nil { + return 0, 0, err + } + defer f.Close() + + var waiting, stalled int64 + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Bytes() + parts := bytes.Split(line, []byte(" ")) + if len(parts) != 5 { + continue + } + _, value, err := parseKVPair(parts[4]) + if err != nil { + continue + } + switch { + case bytes.Equal(parts[0], []byte("some")): + waiting = value + case bytes.Equal(parts[0], []byte("full")): + stalled = value + } + } + return waiting, stalled, scanner.Err() +} + func splitServiceName(path string) (string, string) { slice, name := filepath.Split(path) slice = strings.Trim(slice, "/")