Repository: prometheus/procfs
Pull request #597 (2023-12-13 19:00:09)

Add support for guest_time and cguest_time stats.
This PR adds support for the `guest_time` and `cguest_time` fields from `/proc/<pid>/stat`. These are useful for monitoring kvm guest processes. 


Patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index ffa6b30..8f25279 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -12,15 +12,21 @@ on:
       - ".golangci.yml"
   pull_request:
 
+permissions:  # added using https://github.com/step-security/secure-repo
+  contents: read
+
 jobs:
   golangci:
+    permissions:
+      contents: read  # for actions/checkout to fetch code
+      pull-requests: read  # for golangci/golangci-lint-action to fetch pull requests
     name: lint
     runs-on: ubuntu-latest
     steps:
       - name: Checkout repository
         uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       - name: install Go
-        uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
+        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
         with:
           go-version: 1.21.x
       - name: Install snmp_exporter/generator dependencies
@@ -29,4 +35,4 @@ jobs:
       - name: Lint
         uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0
         with:
-          version: v1.54.2
+          version: v1.55.2
diff --git a/Makefile.common b/Makefile.common
index bc2a07d..5fd1782 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -62,10 +62,10 @@ SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
 GOLANGCI_LINT_VERSION ?= v1.55.2
-# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
+# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
-	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
+	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
 		# If we're in CI and there is an Actions file, that means the linter
 		# is being run in Actions, so we don't need to run it here.
 		ifneq (,$(SKIP_GOLANGCI_LINT))
diff --git a/btrfs/btrfs.go b/btrfs/btrfs.go
index 094c898..5b4ff35 100644
--- a/btrfs/btrfs.go
+++ b/btrfs/btrfs.go
@@ -25,6 +25,7 @@ type Stats struct {
 	NodeSize       uint64
 	QuotaOverride  uint64
 	SectorSize     uint64
+	CommitStats    CommitStats
 }
 
 // Allocation contains allocation statistics for data, metadata and system data.
@@ -65,3 +66,12 @@ type LayoutUsage struct {
 type Device struct {
 	Size uint64
 }
+
+// Number of commits and various time related statistics.
+// See Linux fs/btrfs/sysfs.c with 6.x version.
+type CommitStats struct {
+	Commits       uint64
+	LastCommitMs  uint64
+	MaxCommitMs   uint64
+	TotalCommitMs uint64
+}
diff --git a/btrfs/get.go b/btrfs/get.go
index 77c72e1..db0046b 100644
--- a/btrfs/get.go
+++ b/btrfs/get.go
@@ -14,6 +14,8 @@
 package btrfs
 
 import (
+	"bufio"
+	"fmt"
 	"os"
 	"path"
 	"path/filepath"
@@ -245,6 +247,62 @@ func (r *reader) readFilesystemStats() (s *Stats) {
 			Metadata:          r.readAllocationStats("allocation/metadata"),
 			System:            r.readAllocationStats("allocation/system"),
 		},
+
+		// Read commit stats data
+		CommitStats: r.readCommitStats("commit_stats"),
 	}
 	return
 }
+
+// readCommitStats returns the commit_stats information for commit stats metrics.
+func (r *reader) readCommitStats(p string) CommitStats {
+	stats := CommitStats{}
+
+	f, err := os.Open(path.Join(r.path, p))
+	if err != nil {
+		// if commit_stats not found. maybe btrfs version < 6.0
+		if !os.IsNotExist(err) {
+			r.err = err
+		}
+		return stats
+	}
+	defer f.Close()
+
+	scanner := bufio.NewScanner(f)
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		parts := strings.Fields(scanner.Text())
+		// require  <key> <value>
+		if len(parts) != 2 {
+			r.err = fmt.Errorf("invalid commit_stats line %q", line)
+			return stats
+		}
+
+		value, err := strconv.ParseUint(parts[1], 10, 64)
+		if err != nil {
+			r.err = fmt.Errorf("error parsing commit_stats line: %w", err)
+			return stats
+		}
+
+		switch metricName := parts[0]; metricName {
+		case "commits":
+			stats.Commits = value
+		case "last_commit_ms":
+			stats.LastCommitMs = value
+		case "max_commit_ms":
+			stats.MaxCommitMs = value
+		case "total_commit_ms":
+			stats.TotalCommitMs = value
+		default:
+			continue
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		r.err = fmt.Errorf("error scanning commit_stats file: %w", err)
+		return stats
+	}
+
+	return stats
+}
diff --git a/go.mod b/go.mod
index 56ba14c..1e58b3a 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,6 @@ go 1.19
 
 require (
 	github.com/google/go-cmp v0.6.0
-	golang.org/x/sync v0.5.0
-	golang.org/x/sys v0.13.0
+	golang.org/x/sync v0.6.0
+	golang.org/x/sys v0.16.0
 )
diff --git a/go.sum b/go.sum
index cabf297..caa1dc9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/meminfo.go b/meminfo.go
index db1daea..73a03e8 100644
--- a/meminfo.go
+++ b/meminfo.go
@@ -126,6 +126,7 @@ type Meminfo struct {
 	VmallocUsed *uint64
 	// largest contiguous block of vmalloc area which is free
 	VmallocChunk      *uint64
+	Percpu            *uint64
 	HardwareCorrupted *uint64
 	AnonHugePages     *uint64
 	ShmemHugePages    *uint64
@@ -178,6 +179,7 @@ type Meminfo struct {
 	VmallocTotalBytes      *uint64
 	VmallocUsedBytes       *uint64
 	VmallocChunkBytes      *uint64
+	PercpuBytes            *uint64
 	HardwareCorruptedBytes *uint64
 	AnonHugePagesBytes     *uint64
 	ShmemHugePagesBytes    *uint64
@@ -339,6 +341,9 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
 		case "VmallocChunk:":
 			m.VmallocChunk = &val
 			m.VmallocChunkBytes = &valBytes
+		case "Percpu:":
+			m.Percpu = &val
+			m.PercpuBytes = &valBytes
 		case "HardwareCorrupted:":
 			m.HardwareCorrupted = &val
 			m.HardwareCorruptedBytes = &valBytes
diff --git a/proc_stat.go b/proc_stat.go
index 923e550..06a8d93 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -110,6 +110,11 @@ type ProcStat struct {
 	Policy uint
 	// Aggregated block I/O delays, measured in clock ticks (centiseconds).
 	DelayAcctBlkIOTicks uint64
+	// Guest time of the process (time spent running a virtual CPU for a guest
+	// operating system), measured in clock ticks.
+	GuestTime int
+	// Guest time of the process's children, measured in clock ticks.
+	CGuestTime int
 
 	proc FS
 }
@@ -189,6 +194,8 @@ func (p Proc) Stat() (ProcStat, error) {
 		&s.RTPriority,
 		&s.Policy,
 		&s.DelayAcctBlkIOTicks,
+		&s.GuestTime,
+		&s.CGuestTime,
 	)
 	if err != nil {
 		return ProcStat{}, err
diff --git a/sysfs/class_infiniband.go b/sysfs/class_infiniband.go
index 90dc0d0..7892c69 100644
--- a/sysfs/class_infiniband.go
+++ b/sysfs/class_infiniband.go
@@ -279,13 +279,16 @@ func (fs FS) parseInfiniBandPort(name string, port string) (*InfiniBandPort, err
 		return nil, fmt.Errorf("could not parse rate file in %q: %w", portPath, err)
 	}
 
-	counters, err := parseInfiniBandCounters(portPath)
-	if err != nil {
-		return nil, err
+	// Intel irdma module does not expose /sys/class/infiniband/<device>/ports/<port-num>/counters
+	if !strings.HasPrefix(ibp.Name, "irdma") {
+		counters, err := parseInfiniBandCounters(portPath)
+		if err != nil {
+			return nil, err
+		}
+		ibp.Counters = *counters
 	}
-	ibp.Counters = *counters
 
-	if strings.Contains(ibp.Name, "mlx5") {
+	if strings.HasPrefix(ibp.Name, "irdma") || strings.HasPrefix(ibp.Name, "mlx5_") {
 		hwCounters, err := parseInfiniBandHwCounters(portPath)
 		if err != nil {
 			return nil, err
@@ -296,6 +299,9 @@ func (fs FS) parseInfiniBandPort(name string, port string) (*InfiniBandPort, err
 	return &ibp, nil
 }
 
+// parseInfiniBandCounters parses the counters exposed under
+// /sys/class/infiniband/<device>/ports/<port-num>/counters, which first appeared in kernel v2.6.12.
+// Prior to kernel v4.5, 64-bit counters were exposed separately under the "counters_ext" directory.
 func parseInfiniBandCounters(portPath string) (*InfiniBandCounters, error) {
 	var counters InfiniBandCounters
 
@@ -393,7 +399,7 @@ func parseInfiniBandCounters(portPath string) (*InfiniBandCounters, error) {
 		}
 	}
 
-	// Parse legacy counters
+	// Parse pre-kernel-v4.5 64-bit counters.
 	path = filepath.Join(portPath, "counters_ext")
 	files, err = os.ReadDir(path)
 	if err != nil && !os.IsNotExist(err) {
@@ -457,6 +463,8 @@ func parseInfiniBandCounters(portPath string) (*InfiniBandCounters, error) {
 	return &counters, nil
 }
 
+// parseInfiniBandHwCounters parses the optional counters exposed under
+// /sys/class/infiniband/<device>/ports/<port-num>/hw_counters, which first appeared in kernel v4.6.
 func parseInfiniBandHwCounters(portPath string) (*InfiniBandHwCounters, error) {
 	var hwCounters InfiniBandHwCounters
 


Test patch:
diff --git a/btrfs/get_test.go b/btrfs/get_test.go
index 23059c6..e2d09e1 100644
--- a/btrfs/get_test.go
+++ b/btrfs/get_test.go
@@ -19,6 +19,7 @@ type testVector struct {
 	uuid, label        string
 	devices, features  int
 	data, meta, system alloc
+	commitstats        commit
 }
 
 type alloc struct {
@@ -27,6 +28,13 @@ type alloc struct {
 	ratio  float64
 }
 
+type commit struct {
+	commits       uint64
+	lastCommitMs  uint64
+	maxCommitMs   uint64
+	totalCommitMs uint64
+}
+
 func TestFSBtrfsStats(t *testing.T) {
 	btrfs, err := NewFS("testdata/fixtures/sys")
 	if err != nil {
@@ -39,22 +47,24 @@ func TestFSBtrfsStats(t *testing.T) {
 
 	tests := []testVector{
 		{
-			uuid:     "0abb23a9-579b-43e6-ad30-227ef47fcb9d",
-			label:    "fixture",
-			devices:  2,
-			features: 4,
-			data:     alloc{"raid0", 2147483648, 1},
-			meta:     alloc{"raid1", 1073741824, 2},
-			system:   alloc{"raid1", 8388608, 2},
+			uuid:        "0abb23a9-579b-43e6-ad30-227ef47fcb9d",
+			label:       "fixture",
+			devices:     2,
+			features:    4,
+			data:        alloc{"raid0", 2147483648, 1},
+			meta:        alloc{"raid1", 1073741824, 2},
+			system:      alloc{"raid1", 8388608, 2},
+			commitstats: commit{258051, 1000, 51462, 47836090},
 		},
 		{
-			uuid:     "7f07c59f-6136-449c-ab87-e1cf2328731b",
-			label:    "",
-			devices:  4,
-			features: 5,
-			data:     alloc{"raid5", 644087808, 4. / 3.},
-			meta:     alloc{"raid6", 429391872, 4. / 2.},
-			system:   alloc{"raid6", 16777216, 4. / 2.},
+			uuid:        "7f07c59f-6136-449c-ab87-e1cf2328731b",
+			label:       "",
+			devices:     4,
+			features:    5,
+			data:        alloc{"raid5", 644087808, 4. / 3.},
+			meta:        alloc{"raid6", 429391872, 4. / 2.},
+			system:      alloc{"raid6", 16777216, 4. / 2.},
+			commitstats: commit{0, 0, 0, 0},
 		},
 	}
 
@@ -98,5 +108,21 @@ func TestFSBtrfsStats(t *testing.T) {
 		if want, got := tt.system.ratio, stats[i].Allocation.System.Layouts[tt.system.layout].Ratio; want != got {
 			t.Errorf("fs %q unexpected system ratio:\nwant: %f\nhave: %f", tt.uuid, want, got)
 		}
+
+		if want, got := tt.commitstats.commits, stats[i].CommitStats.Commits; want != got {
+			t.Errorf("fs %q unexpected commit stats commits:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
+
+		if want, got := tt.commitstats.lastCommitMs, stats[i].CommitStats.LastCommitMs; want != got {
+			t.Errorf("fs %q unexpected commit stats last_commit_ms:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
+
+		if want, got := tt.commitstats.maxCommitMs, stats[i].CommitStats.MaxCommitMs; want != got {
+			t.Errorf("fs %q unexpected commit stats max_commit_ms:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
+
+		if want, got := tt.commitstats.totalCommitMs, stats[i].CommitStats.TotalCommitMs; want != got {
+			t.Errorf("fs %q unexpected commit stats total_commit_ms:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
 	}
 }
diff --git a/meminfo_test.go b/meminfo_test.go
index 2d2b238..3d3b409 100644
--- a/meminfo_test.go
+++ b/meminfo_test.go
@@ -53,6 +53,7 @@ func TestMeminfo(t *testing.T) {
 		VmallocTotal:      newuint64(34359738367),
 		VmallocUsed:       newuint64(36596),
 		VmallocChunk:      newuint64(34359637840),
+		Percpu:            newuint64(26176),
 		HardwareCorrupted: newuint64(0),
 		AnonHugePages:     newuint64(12288),
 		HugePagesTotal:    newuint64(0),
@@ -96,6 +97,7 @@ func TestMeminfo(t *testing.T) {
 		VmallocTotalBytes:      newuint64(35184372087808),
 		VmallocUsedBytes:       newuint64(37474304),
 		VmallocChunkBytes:      newuint64(35184269148160),
+		PercpuBytes:            newuint64(26804224),
 		HardwareCorruptedBytes: newuint64(0),
 		AnonHugePagesBytes:     newuint64(12582912),
 		HugepagesizeBytes:      newuint64(2097152),
diff --git a/net_tcp_test.go b/net_tcp_test.go
index a32e2fb..5ff33ef 100644
--- a/net_tcp_test.go
+++ b/net_tcp_test.go
@@ -30,7 +30,7 @@ func Test_newNetTCP(t *testing.T) {
 			name: "tcp file found, no error should come up",
 			file: "testdata/fixtures/proc/net/tcp",
 			want: []*netIPSocketLine{
-				&netIPSocketLine{
+				{
 					Sl:        0,
 					LocalAddr: net.IP{10, 0, 0, 5},
 					LocalPort: 22,
@@ -42,7 +42,7 @@ func Test_newNetTCP(t *testing.T) {
 					UID:       0,
 					Inode:     2740,
 				},
-				&netIPSocketLine{
+				{
 					Sl:        1,
 					LocalAddr: net.IP{0, 0, 0, 0},
 					LocalPort: 22,
@@ -54,7 +54,7 @@ func Test_newNetTCP(t *testing.T) {
 					UID:       0,
 					Inode:     2740,
 				},
-				&netIPSocketLine{
+				{
 					Sl:        2,
 					LocalAddr: net.IP{0, 0, 0, 0},
 					LocalPort: 22,
@@ -73,7 +73,7 @@ func Test_newNetTCP(t *testing.T) {
 			name: "tcp6 file found, no error should come up",
 			file: "testdata/fixtures/proc/net/tcp6",
 			want: []*netIPSocketLine{
-				&netIPSocketLine{
+				{
 					Sl:        1315,
 					LocalAddr: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 					LocalPort: 5355,
@@ -85,7 +85,7 @@ func Test_newNetTCP(t *testing.T) {
 					UID:       981,
 					Inode:     21040,
 				},
-				&netIPSocketLine{
+				{
 					Sl:        6073,
 					LocalAddr: net.IP{254, 128, 0, 0, 0, 0, 0, 0, 86, 225, 173, 255, 254, 124, 102, 9},
 					LocalPort: 51073,
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index 92f72fa..b22807a 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -2317,7 +2317,7 @@ unused devices: <none>
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/meminfo
-Lines: 42
+Lines: 43
 MemTotal:       15666184 kB
 MemFree:          440324 kB
 Buffers:         1020128 kB
@@ -2351,6 +2351,7 @@ Committed_AS:     530844 kB
 VmallocTotal:   34359738367 kB
 VmallocUsed:       36596 kB
 VmallocChunk:   34359637840 kB
+Percpu:            26176 kB
 HardwareCorrupted:     0 kB
 AnonHugePages:     12288 kB
 HugePages_Total:       0
@@ -14229,6 +14230,14 @@ Lines: 1
 4096
 Mode: 444
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/fs/btrfs/0abb23a9-579b-43e6-ad30-227ef47fcb9d/commit_stats
+Lines: 4
+commits 258051
+last_commit_ms 1000
+max_commit_ms 51462
+total_commit_ms 47836090EOF
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/fs/btrfs/0abb23a9-579b-43e6-ad30-227ef47fcb9d/devices
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -