Repository: prometheus/procfs
Pull request #487 (2023-01-11 12:09:00)

add avgRTT to nfs mountstats
Hi Team,
I'm sending this PR to add avgRTT to nfs mount stats. https://github.com/prometheus/node_exporter/issues/2550

I followed the same approach as linux kernel to calculate `avgRTT`. https://git.linux-nfs.org/?p=bfields/nfs-utils.git;a=blob;f=tools/nfs-iostat/nfs-iostat.py;h=1df74ba822b59f68c556530e6bf825a1e57f611d;hb=HEAD#l342

Let me know your feedback.

cc : @discordianfish 

Thanks!

discordianfish: DCO sign-off missing but LGTM otherwise

Patch:
diff --git a/Makefile.common b/Makefile.common
index 6d8007c..1985fcd 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -61,7 +61,7 @@ PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_
 SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.51.2
+GOLANGCI_LINT_VERSION ?= v1.50.1
 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
diff --git a/fs.go b/fs.go
index df9044e..0102ab0 100644
--- a/fs.go
+++ b/fs.go
@@ -14,8 +14,6 @@
 package procfs
 
 import (
-	"syscall"
-
 	"github.com/prometheus/procfs/internal/fs"
 )
 
@@ -23,7 +21,6 @@ import (
 // kernel data structures.
 type FS struct {
 	proc fs.FS
-	real bool
 }
 
 // DefaultMountPoint is the common mount point of the proc filesystem.
@@ -42,23 +39,5 @@ func NewFS(mountPoint string) (FS, error) {
 	if err != nil {
 		return FS{}, err
 	}
-
-	real, err := isRealProc(mountPoint)
-	if err != nil {
-		return FS{}, err
-	}
-
-	return FS{fs, real}, nil
-}
-
-// isRealProc determines whether supplied mountpoint is really a proc filesystem.
-func isRealProc(mountPoint string) (bool, error) {
-	stat := syscall.Statfs_t{}
-	err := syscall.Statfs(mountPoint, &stat)
-	if err != nil {
-		return false, err
-	}
-
-	// 0x9fa0 is PROC_SUPER_MAGIC: https://elixir.bootlin.com/linux/v6.1/source/include/uapi/linux/magic.h#L87
-	return stat.Type == 0x9fa0, nil
+	return FS{fs}, nil
 }
diff --git a/go.mod b/go.mod
index 7ae864d..d88b439 100644
--- a/go.mod
+++ b/go.mod
@@ -5,5 +5,5 @@ go 1.18
 require (
 	github.com/google/go-cmp v0.5.9
 	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.5.0
+	golang.org/x/sys v0.3.0
 )
diff --git a/go.sum b/go.sum
index b6a2cec..f7fa868 100644
--- a/go.sum
+++ b/go.sum
@@ -2,5 +2,5 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/mountstats.go b/mountstats.go
index 0c482c1..7f68890 100644
--- a/mountstats.go
+++ b/mountstats.go
@@ -186,6 +186,8 @@ type NFSOperationStats struct {
 	CumulativeTotalResponseMilliseconds uint64
 	// Duration from when a request was enqueued to when it was completely handled.
 	CumulativeTotalRequestMilliseconds uint64
+	// The average time from the point the client sends RPC requests until it receives the response.
+	AverageRTTMilliseconds float64
 	// The count of operations that complete with tk_status < 0.  These statuses usually indicate error conditions.
 	Errors uint64
 }
@@ -534,7 +536,6 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
 
 			ns = append(ns, n)
 		}
-
 		opStats := NFSOperationStats{
 			Operation:                           strings.TrimSuffix(ss[0], ":"),
 			Requests:                            ns[0],
@@ -546,6 +547,9 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
 			CumulativeTotalResponseMilliseconds: ns[6],
 			CumulativeTotalRequestMilliseconds:  ns[7],
 		}
+		if ns[0] != 0 {
+			opStats.AverageRTTMilliseconds = float64(ns[6]) / float64(ns[0])
+		}
 
 		if len(ns) > 8 {
 			opStats.Errors = ns[8]
diff --git a/net_wireless.go b/net_wireless.go
deleted file mode 100644
index c80fb15..0000000
--- a/net_wireless.go
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright 2023 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.
-
-package procfs
-
-import (
-	"bufio"
-	"bytes"
-	"fmt"
-	"io"
-	"strconv"
-	"strings"
-
-	"github.com/prometheus/procfs/internal/util"
-)
-
-// Wireless models the content of /proc/net/wireless.
-type Wireless struct {
-	Name string
-
-	// Status is the current 4-digit hex value status of the interface.
-	Status uint64
-
-	// QualityLink is the link quality.
-	QualityLink int
-
-	// QualityLevel is the signal gain (dBm).
-	QualityLevel int
-
-	// QualityNoise is the signal noise baseline (dBm).
-	QualityNoise int
-
-	// DiscardedNwid is the number of discarded packets with wrong nwid/essid.
-	DiscardedNwid int
-
-	// DiscardedCrypt is the number of discarded packets with wrong code/decode (WEP).
-	DiscardedCrypt int
-
-	// DiscardedFrag is the number of discarded packets that can't perform MAC reassembly.
-	DiscardedFrag int
-
-	// DiscardedRetry is the number of discarded packets that reached max MAC retries.
-	DiscardedRetry int
-
-	// DiscardedMisc is the number of discarded packets for other reasons.
-	DiscardedMisc int
-
-	// MissedBeacon is the number of missed beacons/superframe.
-	MissedBeacon int
-}
-
-// Wireless returns kernel wireless statistics.
-func (fs FS) Wireless() ([]*Wireless, error) {
-	b, err := util.ReadFileNoStat(fs.proc.Path("net/wireless"))
-	if err != nil {
-		return nil, err
-	}
-
-	m, err := parseWireless(bytes.NewReader(b))
-	if err != nil {
-		return nil, fmt.Errorf("failed to parse wireless: %w", err)
-	}
-
-	return m, nil
-}
-
-// parseWireless parses the contents of /proc/net/wireless.
-/*
-Inter-| sta-|   Quality        |   Discarded packets               | Missed | WE
-face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon | 22
- eth1: 0000    5.  -256.  -10.       0      1      0     3      0        0
- eth2: 0000    5.  -256.  -20.       0      2      0     4      0        0
-*/
-func parseWireless(r io.Reader) ([]*Wireless, error) {
-	var (
-		interfaces []*Wireless
-		scanner    = bufio.NewScanner(r)
-	)
-
-	for n := 0; scanner.Scan(); n++ {
-		// Skip the 2 header lines.
-		if n < 2 {
-			continue
-		}
-
-		line := scanner.Text()
-
-		parts := strings.Split(line, ":")
-		if len(parts) != 2 {
-			return nil, fmt.Errorf("expected 2 parts after splitting line by ':', got %d for line %q", len(parts), line)
-		}
-
-		name := strings.TrimSpace(parts[0])
-		stats := strings.Fields(parts[1])
-
-		if len(stats) < 10 {
-			return nil, fmt.Errorf("invalid number of fields in line %d, expected at least 10, got %d: %q", n, len(stats), line)
-		}
-
-		status, err := strconv.ParseUint(stats[0], 16, 16)
-		if err != nil {
-			return nil, fmt.Errorf("invalid status in line %d: %q", n, line)
-		}
-
-		qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], "."))
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Quality:link as integer %q: %w", qlink, err)
-		}
-
-		qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], "."))
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Quality:level as integer %q: %w", qlevel, err)
-		}
-
-		qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], "."))
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Quality:noise as integer %q: %w", qnoise, err)
-		}
-
-		dnwid, err := strconv.Atoi(stats[4])
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:nwid as integer %q: %w", dnwid, err)
-		}
-
-		dcrypt, err := strconv.Atoi(stats[5])
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:crypt as integer %q: %w", dcrypt, err)
-		}
-
-		dfrag, err := strconv.Atoi(stats[6])
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:frag as integer %q: %w", dfrag, err)
-		}
-
-		dretry, err := strconv.Atoi(stats[7])
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:retry as integer %q: %w", dretry, err)
-		}
-
-		dmisc, err := strconv.Atoi(stats[8])
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:misc as integer %q: %w", dmisc, err)
-		}
-
-		mbeacon, err := strconv.Atoi(stats[9])
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse Missed:beacon as integer %q: %w", mbeacon, err)
-		}
-
-		w := &Wireless{
-			Name:           name,
-			Status:         status,
-			QualityLink:    qlink,
-			QualityLevel:   qlevel,
-			QualityNoise:   qnoise,
-			DiscardedNwid:  dnwid,
-			DiscardedCrypt: dcrypt,
-			DiscardedFrag:  dfrag,
-			DiscardedRetry: dretry,
-			DiscardedMisc:  dmisc,
-			MissedBeacon:   mbeacon,
-		}
-
-		interfaces = append(interfaces, w)
-	}
-
-	if err := scanner.Err(); err != nil {
-		return nil, fmt.Errorf("failed to scan /proc/net/wireless: %w", err)
-	}
-
-	return interfaces, nil
-}
diff --git a/proc.go b/proc.go
index 48f39da..c30223a 100644
--- a/proc.go
+++ b/proc.go
@@ -21,6 +21,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/prometheus/procfs/internal/fs"
 	"github.com/prometheus/procfs/internal/util"
 )
 
@@ -29,7 +30,7 @@ type Proc struct {
 	// The process ID.
 	PID int
 
-	fs FS
+	fs fs.FS
 }
 
 // Procs represents a list of Proc structs.
@@ -91,7 +92,7 @@ func (fs FS) Proc(pid int) (Proc, error) {
 	if _, err := os.Stat(fs.proc.Path(strconv.Itoa(pid))); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: pid, fs: fs}, nil
+	return Proc{PID: pid, fs: fs.proc}, nil
 }
 
 // AllProcs returns a list of all currently available processes.
@@ -113,7 +114,7 @@ func (fs FS) AllProcs() (Procs, error) {
 		if err != nil {
 			continue
 		}
-		p = append(p, Proc{PID: int(pid), fs: fs})
+		p = append(p, Proc{PID: int(pid), fs: fs.proc})
 	}
 
 	return p, nil
@@ -236,19 +237,6 @@ func (p Proc) FileDescriptorTargets() ([]string, error) {
 // FileDescriptorsLen returns the number of currently open file descriptors of
 // a process.
 func (p Proc) FileDescriptorsLen() (int, error) {
-	// Use fast path if available (Linux v6.2): https://github.com/torvalds/linux/commit/f1f1f2569901
-	if p.fs.real {
-		stat, err := os.Stat(p.path("fd"))
-		if err != nil {
-			return 0, err
-		}
-
-		size := stat.Size()
-		if size > 0 {
-			return int(size), nil
-		}
-	}
-
 	fds, err := p.fileDescriptors()
 	if err != nil {
 		return 0, err
@@ -297,7 +285,7 @@ func (p Proc) fileDescriptors() ([]string, error) {
 }
 
 func (p Proc) path(pa ...string) string {
-	return p.fs.proc.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
+	return p.fs.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
 }
 
 // FileDescriptorsInfo retrieves information about all file descriptors of
diff --git a/proc_stat.go b/proc_stat.go
index 14b249f..b278eb2 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/prometheus/procfs/internal/fs"
 	"github.com/prometheus/procfs/internal/util"
 )
 
@@ -111,7 +112,7 @@ type ProcStat struct {
 	// Aggregated block I/O delays, measured in clock ticks (centiseconds).
 	DelayAcctBlkIOTicks uint64
 
-	proc FS
+	proc fs.FS
 }
 
 // NewStat returns the current status information of the process.
@@ -209,7 +210,8 @@ func (s ProcStat) ResidentMemory() int {
 
 // StartTime returns the unix timestamp of the process in seconds.
 func (s ProcStat) StartTime() (float64, error) {
-	stat, err := s.proc.Stat()
+	fs := FS{proc: s.proc}
+	stat, err := fs.Stat()
 	if err != nil {
 		return 0, err
 	}
diff --git a/sysfs/vmstat_numa.go b/sysfs/vmstat_numa.go
index 038a0ee..8a6d095 100644
--- a/sysfs/vmstat_numa.go
+++ b/sysfs/vmstat_numa.go
@@ -145,7 +145,7 @@ func parseVMStatNUMA(r []byte) (VMStat, error) {
 		case "nr_zone_active_anon":
 			vmStat.NrZoneActiveAnon = fv
 		case "nr_zone_inactive_file":
-			vmStat.NrZoneInactiveFile = fv
+			vmStat.NrZoneActiveFile = fv
 		case "nr_zone_active_file":
 			vmStat.NrZoneActiveFile = fv
 		case "nr_zone_unevictable":
diff --git a/thread.go b/thread.go
index 490c147..f08bfc7 100644
--- a/thread.go
+++ b/thread.go
@@ -54,8 +54,7 @@ func (fs FS) AllThreads(pid int) (Procs, error) {
 		if err != nil {
 			continue
 		}
-
-		t = append(t, Proc{PID: int(tid), fs: FS{fsi.FS(taskPath), fs.real}})
+		t = append(t, Proc{PID: int(tid), fs: fsi.FS(taskPath)})
 	}
 
 	return t, nil
@@ -67,13 +66,13 @@ func (fs FS) Thread(pid, tid int) (Proc, error) {
 	if _, err := os.Stat(taskPath); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: tid, fs: FS{fsi.FS(taskPath), fs.real}}, nil
+	return Proc{PID: tid, fs: fsi.FS(taskPath)}, nil
 }
 
 // Thread returns a process for a given TID of Proc.
 func (proc Proc) Thread(tid int) (Proc, error) {
-	tfs := FS{fsi.FS(proc.path("task")), proc.fs.real}
-	if _, err := os.Stat(tfs.proc.Path(strconv.Itoa(tid))); err != nil {
+	tfs := fsi.FS(proc.path("task"))
+	if _, err := os.Stat(tfs.Path(strconv.Itoa(tid))); err != nil {
 		return Proc{}, err
 	}
 	return Proc{PID: tid, fs: tfs}, nil


Test patch:
diff --git a/mountstats_test.go b/mountstats_test.go
index 9fccf84..3d23671 100644
--- a/mountstats_test.go
+++ b/mountstats_test.go
@@ -339,6 +339,7 @@ func TestMountStats(t *testing.T) {
 								CumulativeQueueMilliseconds:         6,
 								CumulativeTotalResponseMilliseconds: 79386,
 								CumulativeTotalRequestMilliseconds:  79407,
+								AverageRTTMilliseconds:              61.16024653312789,
 							},
 							{
 								Operation: "WRITE",
@@ -352,6 +353,7 @@ func TestMountStats(t *testing.T) {
 								CumulativeQueueMilliseconds:         18446743919241604546,
 								CumulativeTotalResponseMilliseconds: 1667369447,
 								CumulativeTotalRequestMilliseconds:  1953587717,
+								AverageRTTMilliseconds:              0.5695744656983355,
 							},
 						},
 						Transport: NFSTransportStats{
diff --git a/net_wireless_test.go b/net_wireless_test.go
deleted file mode 100644
index 25d6a65..0000000
--- a/net_wireless_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright 2023 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.
-
-package procfs
-
-import (
-	"reflect"
-	"testing"
-)
-
-func TestWireless(t *testing.T) {
-	fs, err := NewFS(procTestFixtures)
-	if err != nil {
-		t.Fatalf("failed to open procfs: %v", err)
-	}
-
-	got, err := fs.Wireless()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	expected := []*Wireless{
-		{
-			Name:           "wlan0",
-			Status:         1,
-			QualityLink:    2,
-			QualityLevel:   3,
-			QualityNoise:   4,
-			DiscardedNwid:  5,
-			DiscardedCrypt: 6,
-			DiscardedFrag:  7,
-			DiscardedRetry: 8,
-			DiscardedMisc:  9,
-			MissedBeacon:   10,
-		},
-		{
-			Name:           "wlan1",
-			Status:         16,
-			QualityLink:    9,
-			QualityLevel:   8,
-			QualityNoise:   7,
-			DiscardedNwid:  6,
-			DiscardedCrypt: 5,
-			DiscardedFrag:  4,
-			DiscardedRetry: 3,
-			DiscardedMisc:  2,
-			MissedBeacon:   1,
-		},
-	}
-
-	if len(got) != len(expected) {
-		t.Fatalf("unexpected number of interfaces parsed %d, expected %d", len(got), len(expected))
-	}
-
-	for i, iface := range got {
-		if !reflect.DeepEqual(iface, expected[i]) {
-			t.Errorf("unexpected interface got %+v, expected %+v", iface, expected[i])
-		}
-	}
-}
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index e9a8cd0..a965b11 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -2568,14 +2568,6 @@ Num       RefCount Protocol Flags    Type St Path
 0000000000000000: 00000003 00000000 00000000 0001 03
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-Path: fixtures/proc/net/wireless
-Lines: 4
-Inter-| sta-|   Quality        |   Discarded packets               | Missed
- face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon
- wlan0: 0001    2.    3.    4.       5      6      7      8      9       10
- wlan1: 0010    9     8.    7.       6      5      4      3      2        1
-Mode: 644
-# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/xfrm_stat
 Lines: 28
 XfrmInError                     1
diff --git a/thread_test.go b/thread_test.go
index 1b83046..503be4f 100644
--- a/thread_test.go
+++ b/thread_test.go
@@ -37,7 +37,7 @@ func TestAllThreads(t *testing.T) {
 			t.Errorf("want TID %d, have %d", wantTID, haveTID)
 		}
 		wantFS := fixFS.proc.Path(strconv.Itoa(testPID), "task")
-		haveFS := string(threads[i].fs.proc)
+		haveFS := string(threads[i].fs)
 		if wantFS != haveFS {
 			t.Errorf("want fs %q, have %q", wantFS, haveFS)
 		}