Repository: prometheus/procfs
Pull request #323 (2020-08-29 16:07:12)

Fix bcache stats_total per-device readings
Hi, current versions of Prometheus node_exporter report identical statistics for all backing_devices. This is caused by a bug in this package. The commit fixes it.


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4224ea8..26ed5f5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,7 +34,7 @@ jobs:
     steps:
     - checkout
     - run: sudo pip install codespell
-    - run: codespell --skip=".git,./vendor,ttar,fixtures.ttar,./fixtures,go.mod,go.sum" -L uint,packages\',ded,alo,als,te,sie,hart
+    - run: codespell --skip=".git,./vendor,ttar,fixtures.ttar,./fixtures,go.mod,go.sum" -L uint,packages\',ded,alo,als,te,sie,hart,hda
 
 workflows:
   version: 2
@@ -42,9 +42,9 @@ workflows:
     jobs:
     - test:
         command: default
-        name: linux-1-13
+        name: linux-1-15
         os: linux
-        go_version: "1.13"
+        go_version: "1.15"
     - test:
         command: default
         name: linux-1-14
@@ -52,9 +52,9 @@ workflows:
         go_version: "1.14"
     - test:
         command: no-test
-        name: windows-1-13
+        name: windows-1-15
         os: windows
-        go_version: "1.13"
+        go_version: "1.15"
     - test:
         command: no-test
         name: windows-1-14
diff --git a/Makefile.common b/Makefile.common
index 9320176..3f3d02c 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -78,7 +78,7 @@ ifneq ($(shell which gotestsum),)
 endif
 endif
 
-PROMU_VERSION ?= 0.5.0
+PROMU_VERSION ?= 0.6.0
 PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
 
 GOLANGCI_LINT :=
diff --git a/bcache/get.go b/bcache/get.go
index c4747ea..0040b28 100644
--- a/bcache/get.go
+++ b/bcache/get.go
@@ -459,7 +459,7 @@ func GetStats(uuidPath string, priorityStats bool) (*Stats, error) {
 		bds.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
 
 		// dir <uuidPath>/<bds.Name>/stats_total
-		par.setSubDir("stats_total")
+		par.setSubDir(bds.Name, "stats_total")
 		bds.Total.Bypassed = par.readValue("bypassed")
 		bds.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
 		bds.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
diff --git a/cpuinfo.go b/cpuinfo.go
index b9fb589..1711d73 100644
--- a/cpuinfo.go
+++ b/cpuinfo.go
@@ -313,6 +313,22 @@ func parseCPUInfoS390X(info []byte) ([]CPUInfo, error) {
 				return nil, err
 			}
 			cpuinfo[i].CPUMHz = v
+		case "physical id":
+			cpuinfo[i].PhysicalID = field[1]
+		case "core id":
+			cpuinfo[i].CoreID = field[1]
+		case "cpu cores":
+			v, err := strconv.ParseUint(field[1], 0, 32)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].CPUCores = uint(v)
+		case "siblings":
+			v, err := strconv.ParseUint(field[1], 0, 32)
+			if err != nil {
+				return nil, err
+			}
+			cpuinfo[i].Siblings = uint(v)
 		}
 	}
 
diff --git a/cpuinfo_riscvx.go b/cpuinfo_riscvx.go
new file mode 100644
index 0000000..e83c2e2
--- /dev/null
+++ b/cpuinfo_riscvx.go
@@ -0,0 +1,19 @@
+// Copyright 2020 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.
+
+// +build linux
+// +build riscv riscv64
+
+package procfs
+
+var parseCPUInfo = parseCPUInfoRISCV
diff --git a/fixtures.ttar b/fixtures.ttar
index 12494d7..4eda11e 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -111,7 +111,7 @@ Max core file size        0                    unlimited            bytes
 Max resident set          unlimited            unlimited            bytes
 Max processes             62898                62898                processes
 Max open files            2048                 4096                 files
-Max locked memory         65536                65536                bytes
+Max locked memory         18446744073708503040 18446744073708503040 bytes
 Max address space         8589934592           unlimited            bytes
 Max file locks            unlimited            unlimited            locks
 Max pending signals       62898                62898                signals
@@ -2129,6 +2129,24 @@ Lines: 6
        4    1FB3C        0          1282A8F                0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/protocols
+Lines: 14
+protocol  size sockets  memory press maxhdr  slab module     cl co di ac io in de sh ss gs se re sp bi br ha uh gp em
+PACKET    1344      2      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
+PINGv6    1112      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
+RAWv6     1112      1      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
+UDPLITEv6 1216      0      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
+UDPv6     1216     10      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
+TCPv6     2144   1937  1225378   no     320   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
+UNIX      1024    120      -1   NI       0   yes  kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
+UDP-Lite  1024      0      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
+PING       904      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
+RAW        912      0      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
+UDP       1024     73      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
+TCP       1984  93064  1225378   yes     320   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
+NETLINK   1040     16      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/proc/net/rpc
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -4639,6 +4657,35 @@ Mode: 644
 Directory: fixtures/sys/devices/system
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/node
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/node/node1
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/node/node1/vmstat
+Lines: 6
+nr_free_pages 1
+nr_zone_inactive_anon 2
+nr_zone_active_anon 3
+nr_zone_inactive_file 4
+nr_zone_active_file 5
+nr_zone_unevictable 6
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/node/node2
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/node/node2/vmstat
+Lines: 6
+nr_free_pages 7
+nr_zone_inactive_anon 8
+nr_zone_active_anon 9
+nr_zone_inactive_file 10
+nr_zone_active_file 11
+nr_zone_unevictable 12
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices/system/clocksource
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/mdstat.go b/mdstat.go
index 98e37aa..a5a4ba5 100644
--- a/mdstat.go
+++ b/mdstat.go
@@ -22,8 +22,9 @@ import (
 )
 
 var (
-	statusLineRE   = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[[U_]+\]`)
-	recoveryLineRE = regexp.MustCompile(`\((\d+)/\d+\)`)
+	statusLineRE      = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[[U_]+\]`)
+	recoveryLineRE    = regexp.MustCompile(`\((\d+)/\d+\)`)
+	componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`)
 )
 
 // MDStat holds info parsed from /proc/mdstat.
@@ -44,6 +45,8 @@ type MDStat struct {
 	BlocksTotal int64
 	// Number of blocks on the device that are in sync.
 	BlocksSynced int64
+	// Name of md component devices
+	Devices []string
 }
 
 // MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of
@@ -140,6 +143,7 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
 			DisksTotal:    total,
 			BlocksTotal:   size,
 			BlocksSynced:  syncedBlocks,
+			Devices:       evalComponentDevices(deviceFields),
 		})
 	}
 
@@ -195,3 +199,18 @@ func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, err error) {
 
 	return syncedBlocks, nil
 }
+
+func evalComponentDevices(deviceFields []string) []string {
+	mdComponentDevices := make([]string, 0)
+	if len(deviceFields) > 3 {
+		for _, field := range deviceFields[4:] {
+			match := componentDeviceRE.FindStringSubmatch(field)
+			if match == nil {
+				continue
+			}
+			mdComponentDevices = append(mdComponentDevices, match[1])
+		}
+	}
+
+	return mdComponentDevices
+}
diff --git a/meminfo.go b/meminfo.go
index 50dab4b..2146734 100644
--- a/meminfo.go
+++ b/meminfo.go
@@ -28,9 +28,9 @@ import (
 type Meminfo struct {
 	// Total usable ram (i.e. physical ram minus a few reserved
 	// bits and the kernel binary code)
-	MemTotal uint64
+	MemTotal *uint64
 	// The sum of LowFree+HighFree
-	MemFree uint64
+	MemFree *uint64
 	// An estimate of how much memory is available for starting
 	// new applications, without swapping. Calculated from
 	// MemFree, SReclaimable, the size of the file LRU lists, and
@@ -39,59 +39,59 @@ type Meminfo struct {
 	// well, and that not all reclaimable slab will be
 	// reclaimable, due to items being in use. The impact of those
 	// factors will vary from system to system.
-	MemAvailable uint64
+	MemAvailable *uint64
 	// Relatively temporary storage for raw disk blocks shouldn't
 	// get tremendously large (20MB or so)
-	Buffers uint64
-	Cached  uint64
+	Buffers *uint64
+	Cached  *uint64
 	// Memory that once was swapped out, is swapped back in but
 	// still also is in the swapfile (if memory is needed it
 	// doesn't need to be swapped out AGAIN because it is already
 	// in the swapfile. This saves I/O)
-	SwapCached uint64
+	SwapCached *uint64
 	// Memory that has been used more recently and usually not
 	// reclaimed unless absolutely necessary.
-	Active uint64
+	Active *uint64
 	// Memory which has been less recently used.  It is more
 	// eligible to be reclaimed for other purposes
-	Inactive     uint64
-	ActiveAnon   uint64
-	InactiveAnon uint64
-	ActiveFile   uint64
-	InactiveFile uint64
-	Unevictable  uint64
-	Mlocked      uint64
+	Inactive     *uint64
+	ActiveAnon   *uint64
+	InactiveAnon *uint64
+	ActiveFile   *uint64
+	InactiveFile *uint64
+	Unevictable  *uint64
+	Mlocked      *uint64
 	// total amount of swap space available
-	SwapTotal uint64
+	SwapTotal *uint64
 	// Memory which has been evicted from RAM, and is temporarily
 	// on the disk
-	SwapFree uint64
+	SwapFree *uint64
 	// Memory which is waiting to get written back to the disk
-	Dirty uint64
+	Dirty *uint64
 	// Memory which is actively being written back to the disk
-	Writeback uint64
+	Writeback *uint64
 	// Non-file backed pages mapped into userspace page tables
-	AnonPages uint64
+	AnonPages *uint64
 	// files which have been mapped, such as libraries
-	Mapped uint64
-	Shmem  uint64
+	Mapped *uint64
+	Shmem  *uint64
 	// in-kernel data structures cache
-	Slab uint64
+	Slab *uint64
 	// Part of Slab, that might be reclaimed, such as caches
-	SReclaimable uint64
+	SReclaimable *uint64
 	// Part of Slab, that cannot be reclaimed on memory pressure
-	SUnreclaim  uint64
-	KernelStack uint64
+	SUnreclaim  *uint64
+	KernelStack *uint64
 	// amount of memory dedicated to the lowest level of page
 	// tables.
-	PageTables uint64
+	PageTables *uint64
 	// NFS pages sent to the server, but not yet committed to
 	// stable storage
-	NFSUnstable uint64
+	NFSUnstable *uint64
 	// Memory used for block device "bounce buffers"
-	Bounce uint64
+	Bounce *uint64
 	// Memory used by FUSE for temporary writeback buffers
-	WritebackTmp uint64
+	WritebackTmp *uint64
 	// Based on the overcommit ratio ('vm.overcommit_ratio'),
 	// this is the total amount of  memory currently available to
 	// be allocated on the system. This limit is only adhered to
@@ -105,7 +105,7 @@ type Meminfo struct {
 	// yield a CommitLimit of 7.3G.
 	// For more details, see the memory overcommit documentation
 	// in vm/overcommit-accounting.
-	CommitLimit uint64
+	CommitLimit *uint64
 	// The amount of memory presently allocated on the system.
 	// The committed memory is a sum of all of the memory which
 	// has been allocated by processes, even if it has not been
@@ -119,27 +119,27 @@ type Meminfo struct {
 	// This is useful if one needs to guarantee that processes will
 	// not fail due to lack of memory once that memory has been
 	// successfully allocated.
-	CommittedAS uint64
+	CommittedAS *uint64
 	// total size of vmalloc memory area
-	VmallocTotal uint64
+	VmallocTotal *uint64
 	// amount of vmalloc area which is used
-	VmallocUsed uint64
+	VmallocUsed *uint64
 	// largest contiguous block of vmalloc area which is free
-	VmallocChunk      uint64
-	HardwareCorrupted uint64
-	AnonHugePages     uint64
-	ShmemHugePages    uint64
-	ShmemPmdMapped    uint64
-	CmaTotal          uint64
-	CmaFree           uint64
-	HugePagesTotal    uint64
-	HugePagesFree     uint64
-	HugePagesRsvd     uint64
-	HugePagesSurp     uint64
-	Hugepagesize      uint64
-	DirectMap4k       uint64
-	DirectMap2M       uint64
-	DirectMap1G       uint64
+	VmallocChunk      *uint64
+	HardwareCorrupted *uint64
+	AnonHugePages     *uint64
+	ShmemHugePages    *uint64
+	ShmemPmdMapped    *uint64
+	CmaTotal          *uint64
+	CmaFree           *uint64
+	HugePagesTotal    *uint64
+	HugePagesFree     *uint64
+	HugePagesRsvd     *uint64
+	HugePagesSurp     *uint64
+	Hugepagesize      *uint64
+	DirectMap4k       *uint64
+	DirectMap2M       *uint64
+	DirectMap1G       *uint64
 }
 
 // Meminfo returns an information about current kernel/system memory statistics.
@@ -175,101 +175,101 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
 
 		switch fields[0] {
 		case "MemTotal:":
-			m.MemTotal = v
+			m.MemTotal = &v
 		case "MemFree:":
-			m.MemFree = v
+			m.MemFree = &v
 		case "MemAvailable:":
-			m.MemAvailable = v
+			m.MemAvailable = &v
 		case "Buffers:":
-			m.Buffers = v
+			m.Buffers = &v
 		case "Cached:":
-			m.Cached = v
+			m.Cached = &v
 		case "SwapCached:":
-			m.SwapCached = v
+			m.SwapCached = &v
 		case "Active:":
-			m.Active = v
+			m.Active = &v
 		case "Inactive:":
-			m.Inactive = v
+			m.Inactive = &v
 		case "Active(anon):":
-			m.ActiveAnon = v
+			m.ActiveAnon = &v
 		case "Inactive(anon):":
-			m.InactiveAnon = v
+			m.InactiveAnon = &v
 		case "Active(file):":
-			m.ActiveFile = v
+			m.ActiveFile = &v
 		case "Inactive(file):":
-			m.InactiveFile = v
+			m.InactiveFile = &v
 		case "Unevictable:":
-			m.Unevictable = v
+			m.Unevictable = &v
 		case "Mlocked:":
-			m.Mlocked = v
+			m.Mlocked = &v
 		case "SwapTotal:":
-			m.SwapTotal = v
+			m.SwapTotal = &v
 		case "SwapFree:":
-			m.SwapFree = v
+			m.SwapFree = &v
 		case "Dirty:":
-			m.Dirty = v
+			m.Dirty = &v
 		case "Writeback:":
-			m.Writeback = v
+			m.Writeback = &v
 		case "AnonPages:":
-			m.AnonPages = v
+			m.AnonPages = &v
 		case "Mapped:":
-			m.Mapped = v
+			m.Mapped = &v
 		case "Shmem:":
-			m.Shmem = v
+			m.Shmem = &v
 		case "Slab:":
-			m.Slab = v
+			m.Slab = &v
 		case "SReclaimable:":
-			m.SReclaimable = v
+			m.SReclaimable = &v
 		case "SUnreclaim:":
-			m.SUnreclaim = v
+			m.SUnreclaim = &v
 		case "KernelStack:":
-			m.KernelStack = v
+			m.KernelStack = &v
 		case "PageTables:":
-			m.PageTables = v
+			m.PageTables = &v
 		case "NFS_Unstable:":
-			m.NFSUnstable = v
+			m.NFSUnstable = &v
 		case "Bounce:":
-			m.Bounce = v
+			m.Bounce = &v
 		case "WritebackTmp:":
-			m.WritebackTmp = v
+			m.WritebackTmp = &v
 		case "CommitLimit:":
-			m.CommitLimit = v
+			m.CommitLimit = &v
 		case "Committed_AS:":
-			m.CommittedAS = v
+			m.CommittedAS = &v
 		case "VmallocTotal:":
-			m.VmallocTotal = v
+			m.VmallocTotal = &v
 		case "VmallocUsed:":
-			m.VmallocUsed = v
+			m.VmallocUsed = &v
 		case "VmallocChunk:":
-			m.VmallocChunk = v
+			m.VmallocChunk = &v
 		case "HardwareCorrupted:":
-			m.HardwareCorrupted = v
+			m.HardwareCorrupted = &v
 		case "AnonHugePages:":
-			m.AnonHugePages = v
+			m.AnonHugePages = &v
 		case "ShmemHugePages:":
-			m.ShmemHugePages = v
+			m.ShmemHugePages = &v
 		case "ShmemPmdMapped:":
-			m.ShmemPmdMapped = v
+			m.ShmemPmdMapped = &v
 		case "CmaTotal:":
-			m.CmaTotal = v
+			m.CmaTotal = &v
 		case "CmaFree:":
-			m.CmaFree = v
+			m.CmaFree = &v
 		case "HugePages_Total:":
-			m.HugePagesTotal = v
+			m.HugePagesTotal = &v
 		case "HugePages_Free:":
-			m.HugePagesFree = v
+			m.HugePagesFree = &v
 		case "HugePages_Rsvd:":
-			m.HugePagesRsvd = v
+			m.HugePagesRsvd = &v
 		case "HugePages_Surp:":
-			m.HugePagesSurp = v
+			m.HugePagesSurp = &v
 		case "Hugepagesize:":
-			m.Hugepagesize = v
+			m.Hugepagesize = &v
 		case "DirectMap4k:":
-			m.DirectMap4k = v
+			m.DirectMap4k = &v
 		case "DirectMap2M:":
-			m.DirectMap2M = v
+			m.DirectMap2M = &v
 		case "DirectMap1G:":
-			m.DirectMap1G = v
+			m.DirectMap1G = &v
 		}
 	}
 
diff --git a/mountstats.go b/mountstats.go
index 861ced9..f7a828b 100644
--- a/mountstats.go
+++ b/mountstats.go
@@ -338,12 +338,12 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 		if len(ss) == 0 {
 			break
 		}
-		if len(ss) < 2 {
-			return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
-		}
 
 		switch ss[0] {
 		case fieldOpts:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			if stats.Opts == nil {
 				stats.Opts = map[string]string{}
 			}
@@ -356,6 +356,9 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 				}
 			}
 		case fieldAge:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			// Age integer is in seconds
 			d, err := time.ParseDuration(ss[1] + "s")
 			if err != nil {
@@ -364,6 +367,9 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 
 			stats.Age = d
 		case fieldBytes:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			bstats, err := parseNFSBytesStats(ss[1:])
 			if err != nil {
 				return nil, err
@@ -371,6 +377,9 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 
 			stats.Bytes = *bstats
 		case fieldEvents:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			estats, err := parseNFSEventsStats(ss[1:])
 			if err != nil {
 				return nil, err
diff --git a/net_protocols.go b/net_protocols.go
new file mode 100644
index 0000000..8c6de37
--- /dev/null
+++ b/net_protocols.go
@@ -0,0 +1,180 @@
+// Copyright 2020 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"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// NetProtocolStats stores the contents from /proc/net/protocols
+type NetProtocolStats map[string]NetProtocolStatLine
+
+// NetProtocolStatLine contains a single line parsed from /proc/net/protocols. We
+// only care about the first six columns as the rest are not likely to change
+// and only serve to provide a set of capabilities for each protocol.
+type NetProtocolStatLine struct {
+	Name         string // 0 The name of the protocol
+	Size         uint64 // 1 The size, in bytes, of a given protocol structure. e.g. sizeof(struct tcp_sock) or sizeof(struct unix_sock)
+	Sockets      int64  // 2 Number of sockets in use by this protocol
+	Memory       int64  // 3 Number of 4KB pages allocated by all sockets of this protocol
+	Pressure     int    // 4 This is either yes, no, or NI (not implemented). For the sake of simplicity we treat NI as not experiencing memory pressure.
+	MaxHeader    uint64 // 5 Protocol specific max header size
+	Slab         bool   // 6 Indicates whether or not memory is allocated from the SLAB
+	ModuleName   string // 7 The name of the module that implemented this protocol or "kernel" if not from a module
+	Capabilities NetProtocolCapabilities
+}
+
+// NetProtocolCapabilities contains a list of capabilities for each protocol
+type NetProtocolCapabilities struct {
+	Close               bool // 8
+	Connect             bool // 9
+	Disconnect          bool // 10
+	Accept              bool // 11
+	IoCtl               bool // 12
+	Init                bool // 13
+	Destroy             bool // 14
+	Shutdown            bool // 15
+	SetSockOpt          bool // 16
+	GetSockOpt          bool // 17
+	SendMsg             bool // 18
+	RecvMsg             bool // 19
+	SendPage            bool // 20
+	Bind                bool // 21
+	BacklogRcv          bool // 22
+	Hash                bool // 23
+	UnHash              bool // 24
+	GetPort             bool // 25
+	EnterMemoryPressure bool // 26
+}
+
+// NetProtocols reads stats from /proc/net/protocols and returns a map of
+// PortocolStatLine entries. As of this writing no official Linux Documentation
+// exists, however the source is fairly self-explanatory and the format seems
+// stable since its introduction in 2.6.12-rc2
+// Linux 2.6.12-rc2 - https://elixir.bootlin.com/linux/v2.6.12-rc2/source/net/core/sock.c#L1452
+// Linux 5.10 - https://elixir.bootlin.com/linux/v5.10.4/source/net/core/sock.c#L3586
+func (fs FS) NetProtocols() (NetProtocolStats, error) {
+	data, err := util.ReadFileNoStat(fs.proc.Path("net/protocols"))
+	if err != nil {
+		return NetProtocolStats{}, err
+	}
+	return parseNetProtocols(bufio.NewScanner(bytes.NewReader(data)))
+}
+
+func parseNetProtocols(s *bufio.Scanner) (NetProtocolStats, error) {
+	nps := NetProtocolStats{}
+
+	// Skip the header line
+	s.Scan()
+
+	for s.Scan() {
+		line, err := nps.parseLine(s.Text())
+		if err != nil {
+			return NetProtocolStats{}, err
+		}
+
+		nps[line.Name] = *line
+	}
+	return nps, nil
+}
+
+func (ps NetProtocolStats) parseLine(rawLine string) (*NetProtocolStatLine, error) {
+	line := &NetProtocolStatLine{Capabilities: NetProtocolCapabilities{}}
+	var err error
+	const enabled = "yes"
+	const disabled = "no"
+
+	fields := strings.Fields(rawLine)
+	line.Name = fields[0]
+	line.Size, err = strconv.ParseUint(fields[1], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	line.Sockets, err = strconv.ParseInt(fields[2], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	line.Memory, err = strconv.ParseInt(fields[3], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	if fields[4] == enabled {
+		line.Pressure = 1
+	} else if fields[4] == disabled {
+		line.Pressure = 0
+	} else {
+		line.Pressure = -1
+	}
+	line.MaxHeader, err = strconv.ParseUint(fields[5], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	if fields[6] == enabled {
+		line.Slab = true
+	} else if fields[6] == disabled {
+		line.Slab = false
+	} else {
+		return nil, fmt.Errorf("unable to parse capability for protocol: %s", line.Name)
+	}
+	line.ModuleName = fields[7]
+
+	err = line.Capabilities.parseCapabilities(fields[8:])
+	if err != nil {
+		return nil, err
+	}
+
+	return line, nil
+}
+
+func (pc *NetProtocolCapabilities) parseCapabilities(capabilities []string) error {
+	// The capabilities are all bools so we can loop over to map them
+	capabilityFields := [...]*bool{
+		&pc.Close,
+		&pc.Connect,
+		&pc.Disconnect,
+		&pc.Accept,
+		&pc.IoCtl,
+		&pc.Init,
+		&pc.Destroy,
+		&pc.Shutdown,
+		&pc.SetSockOpt,
+		&pc.GetSockOpt,
+		&pc.SendMsg,
+		&pc.RecvMsg,
+		&pc.SendPage,
+		&pc.Bind,
+		&pc.BacklogRcv,
+		&pc.Hash,
+		&pc.UnHash,
+		&pc.GetPort,
+		&pc.EnterMemoryPressure,
+	}
+
+	for i := 0; i < len(capabilities); i++ {
+		if capabilities[i] == "y" {
+			*capabilityFields[i] = true
+		} else if capabilities[i] == "n" {
+			*capabilityFields[i] = false
+		} else {
+			return fmt.Errorf("unable to parse capability block for protocol: position %d", i)
+		}
+	}
+	return nil
+}
diff --git a/proc_limits.go b/proc_limits.go
index 91ee24d..f28ab9e 100644
--- a/proc_limits.go
+++ b/proc_limits.go
@@ -26,55 +26,55 @@ import (
 // http://man7.org/linux/man-pages/man2/getrlimit.2.html.
 type ProcLimits struct {
 	// CPU time limit in seconds.
-	CPUTime int64
+	CPUTime uint64
 	// Maximum size of files that the process may create.
-	FileSize int64
+	FileSize uint64
 	// Maximum size of the process's data segment (initialized data,
 	// uninitialized data, and heap).
-	DataSize int64
+	DataSize uint64
 	// Maximum size of the process stack in bytes.
-	StackSize int64
+	StackSize uint64
 	// Maximum size of a core file.
-	CoreFileSize int64
+	CoreFileSize uint64
 	// Limit of the process's resident set in pages.
-	ResidentSet int64
+	ResidentSet uint64
 	// Maximum number of processes that can be created for the real user ID of
 	// the calling process.
-	Processes int64
+	Processes uint64
 	// Value one greater than the maximum file descriptor number that can be
 	// opened by this process.
-	OpenFiles int64
+	OpenFiles uint64
 	// Maximum number of bytes of memory that may be locked into RAM.
-	LockedMemory int64
+	LockedMemory uint64
 	// Maximum size of the process's virtual memory address space in bytes.
-	AddressSpace int64
+	AddressSpace uint64
 	// Limit on the combined number of flock(2) locks and fcntl(2) leases that
 	// this process may establish.
-	FileLocks int64
+	FileLocks uint64
 	// Limit of signals that may be queued for the real user ID of the calling
 	// process.
-	PendingSignals int64
+	PendingSignals uint64
 	// Limit on the number of bytes that can be allocated for POSIX message
 	// queues for the real user ID of the calling process.
-	MsqqueueSize int64
+	MsqqueueSize uint64
 	// Limit of the nice priority set using setpriority(2) or nice(2).
-	NicePriority int64
+	NicePriority uint64
 	// Limit of the real-time priority set using sched_setscheduler(2) or
 	// sched_setparam(2).
-	RealtimePriority int64
+	RealtimePriority uint64
 	// Limit (in microseconds) on the amount of CPU time that a process
 	// scheduled under a real-time scheduling policy may consume without making
 	// a blocking system call.
-	RealtimeTimeout int64
+	RealtimeTimeout uint64
 }
 
 const (
-	limitsFields    = 3
+	limitsFields    = 4
 	limitsUnlimited = "unlimited"
 )
 
 var (
-	limitsDelimiter = regexp.MustCompile("  +")
+	limitsMatch = regexp.MustCompile(`(Max \w+\s{0,1}?\w*\s{0,1}\w*)\s{2,}(\w+)\s+(\w+)`)
 )
 
 // NewLimits returns the current soft limits of the process.
@@ -96,46 +96,50 @@ func (p Proc) Limits() (ProcLimits, error) {
 		l = ProcLimits{}
 		s = bufio.NewScanner(f)
 	)
+
+	s.Scan() // Skip limits header
+
 	for s.Scan() {
-		fields := limitsDelimiter.Split(s.Text(), limitsFields)
+		//fields := limitsMatch.Split(s.Text(), limitsFields)
+		fields := limitsMatch.FindStringSubmatch(s.Text())
 		if len(fields) != limitsFields {
 			return ProcLimits{}, fmt.Errorf(
 				"couldn't parse %s line %s", f.Name(), s.Text())
 		}
 
-		switch fields[0] {
+		switch fields[1] {
 		case "Max cpu time":
-			l.CPUTime, err = parseInt(fields[1])
+			l.CPUTime, err = parseUint(fields[2])
 		case "Max file size":
-			l.FileSize, err = parseInt(fields[1])
+			l.FileSize, err = parseUint(fields[2])
 		case "Max data size":
-			l.DataSize, err = parseInt(fields[1])
+			l.DataSize, err = parseUint(fields[2])
 		case "Max stack size":
-			l.StackSize, err = parseInt(fields[1])
+			l.StackSize, err = parseUint(fields[2])
 		case "Max core file size":
-			l.CoreFileSize, err = parseInt(fields[1])
+			l.CoreFileSize, err = parseUint(fields[2])
 		case "Max resident set":
-			l.ResidentSet, err = parseInt(fields[1])
+			l.ResidentSet, err = parseUint(fields[2])
 		case "Max processes":
-			l.Processes, err = parseInt(fields[1])
+			l.Processes, err = parseUint(fields[2])
 		case "Max open files":
-			l.OpenFiles, err = parseInt(fields[1])
+			l.OpenFiles, err = parseUint(fields[2])
 		case "Max locked memory":
-			l.LockedMemory, err = parseInt(fields[1])
+			l.LockedMemory, err = parseUint(fields[2])
 		case "Max address space":
-			l.AddressSpace, err = parseInt(fields[1])
+			l.AddressSpace, err = parseUint(fields[2])
 		case "Max file locks":
-			l.FileLocks, err = parseInt(fields[1])
+			l.FileLocks, err = parseUint(fields[2])
 		case "Max pending signals":
-			l.PendingSignals, err = parseInt(fields[1])
+			l.PendingSignals, err = parseUint(fields[2])
 		case "Max msgqueue size":
-			l.MsqqueueSize, err = parseInt(fields[1])
+			l.MsqqueueSize, err = parseUint(fields[2])
 		case "Max nice priority":
-			l.NicePriority, err = parseInt(fields[1])
+			l.NicePriority, err = parseUint(fields[2])
 		case "Max realtime priority":
-			l.RealtimePriority, err = parseInt(fields[1])
+			l.RealtimePriority, err = parseUint(fields[2])
 		case "Max realtime timeout":
-			l.RealtimeTimeout, err = parseInt(fields[1])
+			l.RealtimeTimeout, err = parseUint(fields[2])
 		}
 		if err != nil {
 			return ProcLimits{}, err
@@ -145,11 +149,11 @@ func (p Proc) Limits() (ProcLimits, error) {
 	return l, s.Err()
 }
 
-func parseInt(s string) (int64, error) {
+func parseUint(s string) (uint64, error) {
 	if s == limitsUnlimited {
-		return -1, nil
+		return 18446744073709551615, nil
 	}
-	i, err := strconv.ParseInt(s, 10, 64)
+	i, err := strconv.ParseUint(s, 10, 64)
 	if err != nil {
 		return 0, fmt.Errorf("couldn't parse value %s: %s", s, err)
 	}
diff --git a/sysfs/vmstat_numa.go b/sysfs/vmstat_numa.go
new file mode 100644
index 0000000..d71cd4a
--- /dev/null
+++ b/sysfs/vmstat_numa.go
@@ -0,0 +1,246 @@
+// Copyright 2020 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 sysfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+var (
+	nodePattern      = "devices/system/node/node[0-9]*"
+	nodeNumberRegexp = regexp.MustCompile(`.*devices/system/node/node([0-9]*)`)
+)
+
+type VMStat struct {
+	NrFreePages                uint64
+	NrZoneInactiveAnon         uint64
+	NrZoneActiveAnon           uint64
+	NrZoneInactiveFile         uint64
+	NrZoneActiveFile           uint64
+	NrZoneUnevictable          uint64
+	NrZoneWritePending         uint64
+	NrMlock                    uint64
+	NrPageTablePages           uint64
+	NrKernelStack              uint64
+	NrBounce                   uint64
+	NrZspages                  uint64
+	NrFreeCma                  uint64
+	NumaHit                    uint64
+	NumaMiss                   uint64
+	NumaForeign                uint64
+	NumaInterleave             uint64
+	NumaLocal                  uint64
+	NumaOther                  uint64
+	NrInactiveAnon             uint64
+	NrActiveAnon               uint64
+	NrInactiveFile             uint64
+	NrActiveFile               uint64
+	NrUnevictable              uint64
+	NrSlabReclaimable          uint64
+	NrSlabUnreclaimable        uint64
+	NrIsolatedAnon             uint64
+	NrIsolatedFile             uint64
+	WorkingsetNodes            uint64
+	WorkingsetRefault          uint64
+	WorkingsetActivate         uint64
+	WorkingsetRestore          uint64
+	WorkingsetNodereclaim      uint64
+	NrAnonPages                uint64
+	NrMapped                   uint64
+	NrFilePages                uint64
+	NrDirty                    uint64
+	NrWriteback                uint64
+	NrWritebackTemp            uint64
+	NrShmem                    uint64
+	NrShmemHugepages           uint64
+	NrShmemPmdmapped           uint64
+	NrFileHugepages            uint64
+	NrFilePmdmapped            uint64
+	NrAnonTransparentHugepages uint64
+	NrVmscanWrite              uint64
+	NrVmscanImmediateReclaim   uint64
+	NrDirtied                  uint64
+	NrWritten                  uint64
+	NrKernelMiscReclaimable    uint64
+	NrFollPinAcquired          uint64
+	NrFollPinReleased          uint64
+}
+
+func (fs FS) VMStatNUMA() (map[int]VMStat, error) {
+	m := make(map[int]VMStat)
+	nodes, err := filepath.Glob(fs.sys.Path(nodePattern))
+	if err != nil {
+		return nil, err
+	}
+
+	for _, node := range nodes {
+		nodeNumbers := nodeNumberRegexp.FindStringSubmatch(node)
+		if len(nodeNumbers) != 2 {
+			continue
+		}
+		nodeNumber, err := strconv.Atoi(nodeNumbers[1])
+		if err != nil {
+			return nil, err
+		}
+		file, err := util.ReadFileNoStat(filepath.Join(node, "vmstat"))
+		if err != nil {
+			return nil, err
+		}
+		nodeStats, err := parseVMStatNUMA(file)
+		if err != nil {
+			return nil, err
+		}
+		m[nodeNumber] = nodeStats
+	}
+	return m, nil
+}
+
+func parseVMStatNUMA(r []byte) (VMStat, error) {
+	var (
+		vmStat  = VMStat{}
+		scanner = bufio.NewScanner(bytes.NewReader(r))
+	)
+
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" {
+			continue
+		}
+		parts := strings.Fields(line)
+		if len(parts) != 2 {
+			return vmStat, fmt.Errorf("line scan did not return 2 fields: %s", line)
+		}
+
+		fv, err := strconv.ParseUint(parts[1], 10, 64)
+		if err != nil {
+			return vmStat, fmt.Errorf("invalid value in vmstat: %w", err)
+		}
+		switch parts[0] {
+		case "nr_free_pages":
+			vmStat.NrFreePages = fv
+		case "nr_zone_inactive_anon":
+			vmStat.NrZoneInactiveAnon = fv
+		case "nr_zone_active_anon":
+			vmStat.NrZoneActiveAnon = fv
+		case "nr_zone_inactive_file":
+			vmStat.NrZoneActiveFile = fv
+		case "nr_zone_active_file":
+			vmStat.NrZoneActiveFile = fv
+		case "nr_zone_unevictable":
+			vmStat.NrZoneUnevictable = fv
+		case "nr_zone_write_pending":
+			vmStat.NrZoneWritePending = fv
+		case "nr_mlock":
+			vmStat.NrMlock = fv
+		case "nr_page_table_pages":
+			vmStat.NrPageTablePages = fv
+		case "nr_kernel_stack":
+			vmStat.NrKernelStack = fv
+		case "nr_bounce":
+			vmStat.NrBounce = fv
+		case "nr_zspages":
+			vmStat.NrZspages = fv
+		case "nr_free_cma":
+			vmStat.NrFreeCma = fv
+		case "numa_hit":
+			vmStat.NumaHit = fv
+		case "numa_miss":
+			vmStat.NumaMiss = fv
+		case "numa_foreign":
+			vmStat.NumaForeign = fv
+		case "numa_interleave":
+			vmStat.NumaInterleave = fv
+		case "numa_local":
+			vmStat.NumaLocal = fv
+		case "numa_other":
+			vmStat.NumaOther = fv
+		case "nr_inactive_anon":
+			vmStat.NrInactiveAnon = fv
+		case "nr_active_anon":
+			vmStat.NrActiveAnon = fv
+		case "nr_inactive_file":
+			vmStat.NrInactiveFile = fv
+		case "nr_active_file":
+			vmStat.NrActiveFile = fv
+		case "nr_unevictable":
+			vmStat.NrUnevictable = fv
+		case "nr_slab_reclaimable":
+			vmStat.NrSlabReclaimable = fv
+		case "nr_slab_unreclaimable":
+			vmStat.NrSlabUnreclaimable = fv
+		case "nr_isolated_anon":
+			vmStat.NrIsolatedAnon = fv
+		case "nr_isolated_file":
+			vmStat.NrIsolatedFile = fv
+		case "workingset_nodes":
+			vmStat.WorkingsetNodes = fv
+		case "workingset_refault":
+			vmStat.WorkingsetRefault = fv
+		case "workingset_activate":
+			vmStat.WorkingsetActivate = fv
+		case "workingset_restore":
+			vmStat.WorkingsetRestore = fv
+		case "workingset_nodereclaim":
+			vmStat.WorkingsetNodereclaim = fv
+		case "nr_anon_pages":
+			vmStat.NrAnonPages = fv
+		case "nr_mapped":
+			vmStat.NrMapped = fv
+		case "nr_file_pages":
+			vmStat.NrFilePages = fv
+		case "nr_dirty":
+			vmStat.NrDirty = fv
+		case "nr_writeback":
+			vmStat.NrWriteback = fv
+		case "nr_writeback_temp":
+			vmStat.NrWritebackTemp = fv
+		case "nr_shmem":
+			vmStat.NrShmem = fv
+		case "nr_shmem_hugepages":
+			vmStat.NrShmemHugepages = fv
+		case "nr_shmem_pmdmapped":
+			vmStat.NrShmemPmdmapped = fv
+		case "nr_file_hugepages":
+			vmStat.NrFileHugepages = fv
+		case "nr_file_pmdmapped":
+			vmStat.NrFilePmdmapped = fv
+		case "nr_anon_transparent_hugepages":
+			vmStat.NrAnonTransparentHugepages = fv
+		case "nr_vmscan_write":
+			vmStat.NrVmscanWrite = fv
+		case "nr_vmscan_immediate_reclaim":
+			vmStat.NrVmscanImmediateReclaim = fv
+		case "nr_dirtied":
+			vmStat.NrDirtied = fv
+		case "nr_written":
+			vmStat.NrWritten = fv
+		case "nr_kernel_misc_reclaimable":
+			vmStat.NrKernelMiscReclaimable = fv
+		case "nr_foll_pin_acquired":
+			vmStat.NrFollPinAcquired = fv
+		case "nr_foll_pin_released":
+			vmStat.NrFollPinReleased = fv
+		}
+
+	}
+	return vmStat, scanner.Err()
+}
diff --git a/xfs/parse.go b/xfs/parse.go
index ba3afea..260501e 100644
--- a/xfs/parse.go
+++ b/xfs/parse.go
@@ -286,8 +286,8 @@ func readWriteStats(us []uint32) (ReadWriteStats, error) {
 	}
 
 	return ReadWriteStats{
-		Read:  us[0],
-		Write: us[1],
+		Write: us[0],
+		Read:  us[1],
 	}, nil
 }
 
diff --git a/xfs/xfs.go b/xfs/xfs.go
index cc8b36b..79bba65 100644
--- a/xfs/xfs.go
+++ b/xfs/xfs.go
@@ -124,8 +124,8 @@ type LogOperationStats struct {
 // ReadWriteStats contains statistics regarding the number of read and write
 // system calls for XFS filesystems.
 type ReadWriteStats struct {
-	Read  uint32
 	Write uint32
+	Read  uint32
 }
 
 // AttributeOperationStats contains statistics regarding manipulation of


Test patch:
diff --git a/cpuinfo_test.go b/cpuinfo_test.go
index bfec15d..9a00d69 100644
--- a/cpuinfo_test.go
+++ b/cpuinfo_test.go
@@ -115,18 +115,34 @@ processor 2: version = FF,  identification = 2733E8,  machine = 2964
 processor 3: version = FF,  identification = 2733E8,  machine = 2964
 
 cpu number      : 0
+physical id     : 2
+core id         : 0
+siblings        : 8
+cpu cores       : 4
 cpu MHz dynamic : 5000
 cpu MHz static  : 5000
 
 cpu number      : 1
+physical id     : 2
+core id         : 0
+siblings        : 8
+cpu cores       : 4
 cpu MHz dynamic : 5000
 cpu MHz static  : 5000
 
 cpu number      : 2
+physical id     : 2
+core id         : 1
+siblings        : 8
+cpu cores       : 4
 cpu MHz dynamic : 5000
 cpu MHz static  : 5000
 
 cpu number      : 3
+physical id     : 2
+core id         : 1
+siblings        : 8
+cpu cores       : 4
 cpu MHz dynamic : 5000
 cpu MHz static  : 5000
 `
@@ -327,6 +343,18 @@ func TestCPUInfoParseS390X(t *testing.T) {
 	if want, have := 5000.0, cpuinfo[2].CPUMHz; want != have {
 		t.Errorf("want cpu MHz %v, have %v", want, have)
 	}
+	if want, have := uint(8), cpuinfo[3].Siblings; want != have {
+		t.Errorf("want siblings %v, have %v", want, have)
+	}
+	if want, have := "1", cpuinfo[3].CoreID; want != have {
+		t.Errorf("want core id %v, have %v", want, have)
+	}
+	if want, have := uint(4), cpuinfo[2].CPUCores; want != have {
+		t.Errorf("want cpu cores %v, have %v", want, have)
+	}
+	if want, have := "2", cpuinfo[2].PhysicalID; want != have {
+		t.Errorf("want physical id %v, have %v", want, have)
+	}
 }
 
 func TestCPUInfoParseMips(t *testing.T) {
diff --git a/mdstat_test.go b/mdstat_test.go
index 5404765..1b9a8ea 100644
--- a/mdstat_test.go
+++ b/mdstat_test.go
@@ -14,6 +14,7 @@
 package procfs
 
 import "testing"
+import "github.com/google/go-cmp/cmp"
 
 func TestFS_MDStat(t *testing.T) {
 	fs := getProcFixtures(t)
@@ -24,30 +25,30 @@ func TestFS_MDStat(t *testing.T) {
 	}
 
 	refs := map[string]MDStat{
-		"md127": {Name: "md127", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 312319552, BlocksSynced: 312319552},
-		"md0":   {Name: "md0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 248896, BlocksSynced: 248896},
-		"md4":   {Name: "md4", ActivityState: "inactive", DisksActive: 0, DisksTotal: 0, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 4883648, BlocksSynced: 4883648},
-		"md6":   {Name: "md6", ActivityState: "recovering", DisksActive: 1, DisksTotal: 2, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 195310144, BlocksSynced: 16775552},
-		"md3":   {Name: "md3", ActivityState: "active", DisksActive: 8, DisksTotal: 8, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 5853468288, BlocksSynced: 5853468288},
-		"md8":   {Name: "md8", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 195310144, BlocksSynced: 16775552},
-		"md7":   {Name: "md7", ActivityState: "active", DisksActive: 3, DisksTotal: 4, DisksFailed: 1, DisksSpare: 0, BlocksTotal: 7813735424, BlocksSynced: 7813735424},
-		"md9":   {Name: "md9", ActivityState: "resyncing", DisksActive: 4, DisksTotal: 4, DisksSpare: 1, DisksFailed: 2, BlocksTotal: 523968, BlocksSynced: 0},
-		"md10":  {Name: "md10", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 314159265, BlocksSynced: 314159265},
-		"md11":  {Name: "md11", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 1, DisksSpare: 2, BlocksTotal: 4190208, BlocksSynced: 0},
-		"md12":  {Name: "md12", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksSpare: 0, DisksFailed: 0, BlocksTotal: 3886394368, BlocksSynced: 3886394368},
-		"md120": {Name: "md120", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 2095104, BlocksSynced: 2095104},
-		"md126": {Name: "md126", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1855870976, BlocksSynced: 1855870976},
-		"md219": {Name: "md219", ActivityState: "inactive", DisksTotal: 0, DisksFailed: 0, DisksActive: 0, DisksSpare: 3, BlocksTotal: 7932, BlocksSynced: 7932},
-		"md00":  {Name: "md00", ActivityState: "active", DisksActive: 1, DisksTotal: 1, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 4186624, BlocksSynced: 4186624},
-		"md101": {Name: "md101", ActivityState: "active", DisksActive: 3, DisksTotal: 3, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 322560, BlocksSynced: 322560},
-		"md201": {Name: "md201", ActivityState: "checking", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1993728, BlocksSynced: 114176},
+		"md127": {Name: "md127", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 312319552, BlocksSynced: 312319552, Devices: []string{"sdi2", "sdj2"}},
+		"md0":   {Name: "md0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 248896, BlocksSynced: 248896, Devices: []string{"sdi1", "sdj1"}},
+		"md4":   {Name: "md4", ActivityState: "inactive", DisksActive: 0, DisksTotal: 0, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 4883648, BlocksSynced: 4883648, Devices: []string{"sda3", "sdb3"}},
+		"md6":   {Name: "md6", ActivityState: "recovering", DisksActive: 1, DisksTotal: 2, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 195310144, BlocksSynced: 16775552, Devices: []string{"sdb2", "sdc", "sda2"}},
+		"md3":   {Name: "md3", ActivityState: "active", DisksActive: 8, DisksTotal: 8, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 5853468288, BlocksSynced: 5853468288, Devices: []string{"sda1", "sdh1", "sdg1", "sdf1", "sde1", "sdd1", "sdc1", "sdb1", "sdd1", "sdd2"}},
+		"md8":   {Name: "md8", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 195310144, BlocksSynced: 16775552, Devices: []string{"sdb1", "sda1", "sdc", "sde"}},
+		"md7":   {Name: "md7", ActivityState: "active", DisksActive: 3, DisksTotal: 4, DisksFailed: 1, DisksSpare: 0, BlocksTotal: 7813735424, BlocksSynced: 7813735424, Devices: []string{"sdb1", "sde1", "sdd1", "sdc1"}},
+		"md9":   {Name: "md9", ActivityState: "resyncing", DisksActive: 4, DisksTotal: 4, DisksSpare: 1, DisksFailed: 2, BlocksTotal: 523968, BlocksSynced: 0, Devices: []string{"sdc2", "sdd2", "sdb2", "sda2", "sde", "sdf", "sdg"}},
+		"md10":  {Name: "md10", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 314159265, BlocksSynced: 314159265, Devices: []string{"sda1", "sdb1"}},
+		"md11":  {Name: "md11", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 1, DisksSpare: 2, BlocksTotal: 4190208, BlocksSynced: 0, Devices: []string{"sdb2", "sdc2", "sdc3", "hda", "ssdc2"}},
+		"md12":  {Name: "md12", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksSpare: 0, DisksFailed: 0, BlocksTotal: 3886394368, BlocksSynced: 3886394368, Devices: []string{"sdc2", "sdd2"}},
+		"md120": {Name: "md120", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 2095104, BlocksSynced: 2095104, Devices: []string{"sda1", "sdb1"}},
+		"md126": {Name: "md126", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1855870976, BlocksSynced: 1855870976, Devices: []string{"sdb", "sdc"}},
+		"md219": {Name: "md219", ActivityState: "inactive", DisksTotal: 0, DisksFailed: 0, DisksActive: 0, DisksSpare: 3, BlocksTotal: 7932, BlocksSynced: 7932, Devices: []string{"sdc", "sda"}},
+		"md00":  {Name: "md00", ActivityState: "active", DisksActive: 1, DisksTotal: 1, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 4186624, BlocksSynced: 4186624, Devices: []string{"xvdb"}},
+		"md101": {Name: "md101", ActivityState: "active", DisksActive: 3, DisksTotal: 3, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 322560, BlocksSynced: 322560, Devices: []string{"sdb", "sdd", "sdc"}},
+		"md201": {Name: "md201", ActivityState: "checking", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1993728, BlocksSynced: 114176, Devices: []string{"sda3", "sdb3"}},
 	}
 
 	if want, have := len(refs), len(mdStats); want != have {
 		t.Errorf("want %d parsed md-devices, have %d", want, have)
 	}
 	for _, md := range mdStats {
-		if want, have := refs[md.Name], md; want != have {
+		if want, have := refs[md.Name], md; !cmp.Equal(want, have) {
 			t.Errorf("%s: want %v, have %v", md.Name, want, have)
 		}
 	}
diff --git a/meminfo_test.go b/meminfo_test.go
index 34b43bd..860f177 100644
--- a/meminfo_test.go
+++ b/meminfo_test.go
@@ -20,48 +20,48 @@ import (
 
 func TestMeminfo(t *testing.T) {
 	expected := Meminfo{
-		MemTotal:          15666184,
-		MemFree:           440324,
-		Buffers:           1020128,
-		Cached:            12007640,
-		SwapCached:        0,
-		Active:            6761276,
-		Inactive:          6532708,
-		ActiveAnon:        267256,
-		InactiveAnon:      268,
-		ActiveFile:        6494020,
-		InactiveFile:      6532440,
-		Unevictable:       0,
-		Mlocked:           0,
-		SwapTotal:         0,
-		SwapFree:          0,
-		Dirty:             768,
-		Writeback:         0,
-		AnonPages:         266216,
-		Mapped:            44204,
-		Shmem:             1308,
-		Slab:              1807264,
-		SReclaimable:      1738124,
-		SUnreclaim:        69140,
-		KernelStack:       1616,
-		PageTables:        5288,
-		NFSUnstable:       0,
-		Bounce:            0,
-		WritebackTmp:      0,
-		CommitLimit:       7833092,
-		CommittedAS:       530844,
-		VmallocTotal:      34359738367,
-		VmallocUsed:       36596,
-		VmallocChunk:      34359637840,
-		HardwareCorrupted: 0,
-		AnonHugePages:     12288,
-		HugePagesTotal:    0,
-		HugePagesFree:     0,
-		HugePagesRsvd:     0,
-		HugePagesSurp:     0,
-		Hugepagesize:      2048,
-		DirectMap4k:       91136,
-		DirectMap2M:       16039936,
+		MemTotal:          newuint64(15666184),
+		MemFree:           newuint64(440324),
+		Buffers:           newuint64(1020128),
+		Cached:            newuint64(12007640),
+		SwapCached:        newuint64(0),
+		Active:            newuint64(6761276),
+		Inactive:          newuint64(6532708),
+		ActiveAnon:        newuint64(267256),
+		InactiveAnon:      newuint64(268),
+		ActiveFile:        newuint64(6494020),
+		InactiveFile:      newuint64(6532440),
+		Unevictable:       newuint64(0),
+		Mlocked:           newuint64(0),
+		SwapTotal:         newuint64(0),
+		SwapFree:          newuint64(0),
+		Dirty:             newuint64(768),
+		Writeback:         newuint64(0),
+		AnonPages:         newuint64(266216),
+		Mapped:            newuint64(44204),
+		Shmem:             newuint64(1308),
+		Slab:              newuint64(1807264),
+		SReclaimable:      newuint64(1738124),
+		SUnreclaim:        newuint64(69140),
+		KernelStack:       newuint64(1616),
+		PageTables:        newuint64(5288),
+		NFSUnstable:       newuint64(0),
+		Bounce:            newuint64(0),
+		WritebackTmp:      newuint64(0),
+		CommitLimit:       newuint64(7833092),
+		CommittedAS:       newuint64(530844),
+		VmallocTotal:      newuint64(34359738367),
+		VmallocUsed:       newuint64(36596),
+		VmallocChunk:      newuint64(34359637840),
+		HardwareCorrupted: newuint64(0),
+		AnonHugePages:     newuint64(12288),
+		HugePagesTotal:    newuint64(0),
+		HugePagesFree:     newuint64(0),
+		HugePagesRsvd:     newuint64(0),
+		HugePagesSurp:     newuint64(0),
+		Hugepagesize:      newuint64(2048),
+		DirectMap4k:       newuint64(91136),
+		DirectMap2M:       newuint64(16039936),
 	}
 
 	have, err := getProcFixtures(t).Meminfo()
diff --git a/mountstats_test.go b/mountstats_test.go
index 146df96..391d50c 100644
--- a/mountstats_test.go
+++ b/mountstats_test.go
@@ -64,7 +64,7 @@ func TestMountStats(t *testing.T) {
 		},
 		{
 			name:    "NFSv4 device with too little info",
-			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nhello",
+			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nopts:",
 			invalid: true,
 		},
 		{
@@ -253,6 +253,26 @@ func TestMountStats(t *testing.T) {
 				},
 			}},
 		},
+		{
+			name: "NFS4.1 device with multiline impl_id OK",
+			s:    "device 192.168.0.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nopts: rw,vers=4.1,rsize=131072,wsize=131072,namlen=255,acregmin=3,acregmax=60,acdirmin=30,acdirmax=60,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.2,fsc,local_lock=none\nage: 1234567\nimpl_id: name='FreeBSD 11.2-STABLE #0 r325575+c9231c7d6bd(HEAD): Mon Nov 18 22:46:47 UTC 2019\nuser@host:/path/to/something'\n,domain='something.org',date='1293840000,0'",
+			mounts: []*Mount{{
+				Device: "192.168.0.1:/srv",
+				Mount:  "/mnt/nfs",
+				Type:   "nfs4",
+				Stats: &MountStatsNFS{
+					StatVersion: "1.1",
+					Opts: map[string]string{"rw": "", "vers": "4.1",
+						"rsize": "131072", "wsize": "131072", "namlen": "255", "acregmin": "3",
+						"acregmax": "60", "acdirmin": "30", "acdirmax": "60", "fsc": "", "hard": "",
+						"proto": "tcp", "timeo": "600", "retrans": "2",
+						"sec": "sys", "clientaddr": "192.168.0.2",
+						"local_lock": "none",
+					},
+					Age: 1234567 * time.Second,
+				},
+			}},
+		},
 		{
 			name: "fixtures/proc OK",
 			mounts: []*Mount{
diff --git a/net_protocols_test.go b/net_protocols_test.go
new file mode 100644
index 0000000..cabbd93
--- /dev/null
+++ b/net_protocols_test.go
@@ -0,0 +1,83 @@
+// Copyright 2020 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 (
+	"strings"
+	"testing"
+)
+
+func TestParseCapabilities(t *testing.T) {
+	rawStr := "y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y\n"
+	have := NetProtocolCapabilities{}
+	err := have.parseCapabilities(strings.Fields(rawStr))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}
+	if want != have {
+		t.Errorf("want %+v\nhave %+v\n", want, have)
+	}
+}
+
+func TestProtocolsParseLine(t *testing.T) {
+	rawStr := "TCP       1984  93064  1225378   no     320   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y\n"
+	protocols := NetProtocolStats{}
+	have, err := protocols.parseLine(rawStr)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := NetProtocolStatLine{"TCP", 1984, 93064, 1225378, 0, 320, true, "kernel", NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}}
+	if want != *have {
+		t.Errorf("want %+v\nhave %+v\n", want, have)
+	}
+}
+
+func TestProtocolsParseProtocols(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	protocolStats, err := fs.NetProtocols()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lines := map[string]NetProtocolStatLine{
+		"PACKET":    {"PACKET", 1344, 2, -1, -1, 0, false, "kernel", NetProtocolCapabilities{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}},
+		"PINGv6":    {"PINGv6", 1112, 0, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, false, true, false, false, true, true, true, true, false, true, true, true, true, true, false}},
+		"RAWv6":     {"RAWv6", 1112, 1, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, true, true, true, true, false, false}},
+		"UDPLITEv6": {"UDPLITEv6", 1216, 0, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, false, false, true, true, true, false}},
+		"UDPv6":     {"UDPv6", 1216, 10, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, false, false, true, true, true, false}},
+		"TCPv6":     {"TCPv6", 2144, 1937, 1225378, 0, 320, true, "kernel", NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}},
+		"UNIX":      {"UNIX", 1024, 120, -1, -1, 0, true, "kernel", NetProtocolCapabilities{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}},
+		"UDP-Lite":  {"UDP-Lite", 1024, 0, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, true, false, false, true, true, true, false}},
+		"PING":      {"PING", 904, 0, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, false, true, false, false, true, true, true, true, false, true, true, true, true, true, false}},
+		"RAW":       {"RAW", 912, 0, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, true, true, true, true, false, false}},
+		"UDP":       {"UDP", 1024, 73, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, true, false, false, true, true, true, false}},
+		"TCP":       {"TCP", 1984, 93064, 1225378, 1, 320, true, "kernel", NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}},
+		"NETLINK":   {"NETLINK", 1040, 16, -1, -1, 0, false, "kernel", NetProtocolCapabilities{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}},
+	}
+
+	if want, have := len(lines), len(protocolStats); want != have {
+		t.Errorf("want %d parsed net/protocols lines, have %d", want, have)
+	}
+	for _, line := range protocolStats {
+		if want, have := lines[line.Name], line; want != have {
+			t.Errorf("%s: want %v, have %v", line.Name, want, have)
+		}
+	}
+}
diff --git a/proc_limits_test.go b/proc_limits_test.go
index 9148e28..bb8ae61 100644
--- a/proc_limits_test.go
+++ b/proc_limits_test.go
@@ -28,10 +28,10 @@ func TestLimits(t *testing.T) {
 
 	for _, test := range []struct {
 		name string
-		want int64
-		have int64
+		want uint64
+		have uint64
 	}{
-		{name: "cpu time", want: -1, have: l.CPUTime},
+		{name: "cpu time", want: 18446744073709551615, have: l.CPUTime},
 		{name: "open files", want: 2048, have: l.OpenFiles},
 		{name: "msgqueue size", want: 819200, have: l.MsqqueueSize},
 		{name: "nice priority", want: 0, have: l.NicePriority},
diff --git a/sysfs/vmstat_numa_test.go b/sysfs/vmstat_numa_test.go
new file mode 100644
index 0000000..64977b9
--- /dev/null
+++ b/sysfs/vmstat_numa_test.go
@@ -0,0 +1,44 @@
+// Copyright 2020 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 sysfs
+
+import (
+	"testing"
+)
+
+func TestParseVMStatNUMA(t *testing.T) {
+	fs, err := NewFS(sysTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	vmstat, err := fs.VMStatNUMA()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if want, got := uint64(1), vmstat[1].NrFreePages; want != got {
+		t.Errorf("want vmstat stat nr_free_pages value %d, got %d", want, got)
+	}
+
+	if want, got := uint64(5), vmstat[1].NrZoneActiveFile; want != got {
+		t.Errorf("want numa stat nr_zone_active_file %d, got %d", want, got)
+	}
+	if want, got := uint64(7), vmstat[2].NrFreePages; want != got {
+		t.Errorf("want vmstat stat nr_free_pages value %d, got %d", want, got)
+	}
+
+	if want, got := uint64(11), vmstat[2].NrZoneActiveFile; want != got {
+		t.Errorf("want numa stat nr_zone_active_file %d, got %d", want, got)
+	}
+}
diff --git a/xfs/parse_test.go b/xfs/parse_test.go
index b799f4b..4989eca 100644
--- a/xfs/parse_test.go
+++ b/xfs/parse_test.go
@@ -199,8 +199,8 @@ func TestParseStats(t *testing.T) {
 			s:    "rw 1 2",
 			stats: &xfs.Stats{
 				ReadWrite: xfs.ReadWriteStats{
-					Read:  1,
-					Write: 2,
+					Write: 1,
+					Read:  2,
 				},
 			},
 		},
@@ -577,8 +577,8 @@ func TestParseStats(t *testing.T) {
 					ForceSleep:        739,
 				},
 				ReadWrite: xfs.ReadWriteStats{
-					Read:  107739,
-					Write: 94045,
+					Write: 107739,
+					Read:  94045,
 				},
 				AttributeOperation: xfs.AttributeOperationStats{
 					Get:    4,