Repository: prometheus/procfs
Pull request #136 (2019-02-20 17:00:35)

Remove dependencies from procfs and sysfs onto xfs, nfs, bcache, and iostats
Make the xfs and nfs packages dependent on procfs
instead of the procfs package dependent on xfs and nfs.
This allows clients to depend on procfs without bringing
in xfs and nfs.

This is a possibility for option 1 of issue #135

simonpasquier: From a quick look, it looks ok to me.
SuperQ: @juliusv @beorn7 Would you please review this refactoring?
beorn7: I'm not familiar with this code. Wouldn't @grobie be the most natural reviewer?
SuperQ: @grobie isn't maintaining this much anymore. We're currently working on finding a new owner.
beorn7: Understood. Currently, I cannot volunteer as a new maintainer for lack of time.

For the time being, I would still prefer if @grobie could have a look here. I guess his lack of time is as bad as mine, but he would need less time to review this as he already knows the code.
discordianfish: @grobie Can you just let us know if you will be reviewing this? I know you wanted to propose doing exactly this IIRC, so naturally you'd be the best person to review this.
If you don't have time, no worries. I'll just do my best to review :)
grobie: I'll give it a look tonight or tomorrow night. Thanks for mentioning.

On Sun, Mar 17, 2019, 10:19 discordianfish <notifications@github.com> wrote:

> @grobie <https://github.com/grobie> Can you just let us know if you will
> be reviewing this? I know you wanted to propose doing exactly this IIRC, so
> naturally you'd be the best person to review this.
> If you don't have time, no worries. I'll just do my best to review :)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/prometheus/procfs/pull/136#issuecomment-473647549>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAANaPpTbKWGv2OmbUyprkGOpXynpSQoks5vXgi6gaJpZM4bFrqx>
> .
>


Patch:
diff --git a/bcache/get.go b/bcache/get.go
index b6d97de..3f955fa 100644
--- a/bcache/get.go
+++ b/bcache/get.go
@@ -24,6 +24,31 @@ import (
 	"strings"
 )
 
+// ReadStats retrieves bcache runtime statistics for each bcache.
+func ReadStats(sysfs string) ([]*Stats, error) {
+	matches, err := filepath.Glob(path.Join(sysfs, "fs/bcache/*-*"))
+	if err != nil {
+		return nil, err
+	}
+
+	stats := make([]*Stats, 0, len(matches))
+	for _, uuidPath := range matches {
+		// "*-*" in glob above indicates the name of the bcache.
+		name := filepath.Base(uuidPath)
+
+		// stats
+		s, err := GetStats(uuidPath)
+		if err != nil {
+			return nil, err
+		}
+
+		s.Name = name
+		stats = append(stats, s)
+	}
+
+	return stats, nil
+}
+
 // ParsePseudoFloat parses the peculiar format produced by bcache's bch_hprint.
 func parsePseudoFloat(str string) (float64, error) {
 	ss := strings.Split(str, ".")
diff --git a/blockdevice/stats.go b/blockdevice/stats.go
new file mode 100644
index 0000000..3131529
--- /dev/null
+++ b/blockdevice/stats.go
@@ -0,0 +1,181 @@
+// Copyright 2018 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 blockdevice
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+	"strings"
+)
+
+// Info contains identifying information for a block device such as a disk drive
+type Info struct {
+	MajorNumber uint32
+	MinorNumber uint32
+	DeviceName  string
+}
+
+// IOStats models the iostats data described in the kernel documentation
+// https://www.kernel.org/doc/Documentation/iostats.txt,
+// https://www.kernel.org/doc/Documentation/block/stat.txt,
+// and https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
+type IOStats struct {
+	// ReadIOs is the number of reads completed successfully.
+	ReadIOs uint64
+	// ReadMerges is the number of reads merged.  Reads and writes
+	// which are adjacent to each other may be merged for efficiency.
+	ReadMerges uint64
+	// ReadSectors is the total number of sectors read successfully.
+	ReadSectors uint64
+	// ReadTicks is the total number of milliseconds spent by all reads.
+	ReadTicks uint64
+	// WriteIOs is the total number of writes completed successfully.
+	WriteIOs uint64
+	// WriteMerges is the number of reads merged.
+	WriteMerges uint64
+	// WriteSectors is the total number of sectors written successfully.
+	WriteSectors uint64
+	// WriteTicks is the total number of milliseconds spent by all writes.
+	WriteTicks uint64
+	// IOsInProgress is number of I/Os currently in progress.
+	IOsInProgress uint64
+	// IOsTotalTicks is the number of milliseconds spent doing I/Os.
+	// This field increases so long as IosInProgress is nonzero.
+	IOsTotalTicks uint64
+	// WeightedIOTicks is the weighted number of milliseconds spent doing I/Os.
+	// This can also be used to estimate average queue wait time for requests.
+	WeightedIOTicks uint64
+	// DiscardIOs is the total number of discards completed successfully.
+	DiscardIOs uint64
+	// DiscardMerges is the number of discards merged.
+	DiscardMerges uint64
+	// DiscardSectors is the total number of sectors discarded successfully.
+	DiscardSectors uint64
+	// DiscardTicks is the total number of milliseconds spent by all discards.
+	DiscardTicks uint64
+}
+
+// Diskstats combines the device Info and IOStats
+type Diskstats struct {
+	Info
+	IOStats
+	// IoStatsCount contains the number of io stats read.  For kernel versions
+	// 4.18+, there should be 18 fields read.  For earlier kernel versions this
+	// will be 14 because the discard values are not available.
+	IoStatsCount int
+}
+
+const (
+	procDiskstatsPath   = "diskstats"
+	procDiskstatsFormat = "%d %d %s %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"
+	sysBlockPath        = "block"
+	sysBlockStatFormat  = "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"
+)
+
+// ReadProcDiskstats reads the diskstats file and returns
+// an array of Diskstats (one per line/device)
+func ReadProcDiskstats(procfs string) ([]Diskstats, error) {
+	file, err := os.Open(path.Join(procfs, procDiskstatsPath))
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	diskstats := []Diskstats{}
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		d := &Diskstats{}
+		d.IoStatsCount, err = fmt.Sscanf(scanner.Text(), procDiskstatsFormat,
+			&d.MajorNumber,
+			&d.MinorNumber,
+			&d.DeviceName,
+			&d.ReadIOs,
+			&d.ReadMerges,
+			&d.ReadSectors,
+			&d.ReadTicks,
+			&d.WriteIOs,
+			&d.WriteMerges,
+			&d.WriteSectors,
+			&d.WriteTicks,
+			&d.IOsInProgress,
+			&d.IOsTotalTicks,
+			&d.WeightedIOTicks,
+			&d.DiscardIOs,
+			&d.DiscardMerges,
+			&d.DiscardSectors,
+			&d.DiscardTicks,
+		)
+		// The io.EOF error can be safely ignored because it just means we read fewer than
+		// the full 18 fields.
+		if err != nil && err != io.EOF {
+			return diskstats, err
+		}
+		if d.IoStatsCount == 14 || d.IoStatsCount == 18 {
+			diskstats = append(diskstats, *d)
+		}
+	}
+	return diskstats, scanner.Err()
+}
+
+// ListSysBlockDevices lists the device names from /sys/block/<dev>
+func ListSysBlockDevices(sysfs string) ([]string, error) {
+	deviceDirs, err := ioutil.ReadDir(path.Join(sysfs, sysBlockPath))
+	if err != nil {
+		return nil, err
+	}
+	devices := []string{}
+	for _, deviceDir := range deviceDirs {
+		if deviceDir.IsDir() {
+			devices = append(devices, deviceDir.Name())
+		}
+	}
+	return devices, nil
+}
+
+// ReadSysBlockDeviceStat returns stats for the block device read from /sys/block/<device>/stat.
+// The number of stats read will be 15 if the discard stats are available (kernel 4.18+)
+// and 11 if they are not available.
+func ReadSysBlockDeviceStat(sysfs string, device string) (IOStats, int, error) {
+	stat := IOStats{}
+	bytes, err := ioutil.ReadFile(path.Join(sysfs, sysBlockPath, device, "stat"))
+	if err != nil {
+		return stat, 0, err
+	}
+	count, err := fmt.Sscanf(strings.TrimSpace(string(bytes)), sysBlockStatFormat,
+		&stat.ReadIOs,
+		&stat.ReadMerges,
+		&stat.ReadSectors,
+		&stat.ReadTicks,
+		&stat.WriteIOs,
+		&stat.WriteMerges,
+		&stat.WriteSectors,
+		&stat.WriteTicks,
+		&stat.IOsInProgress,
+		&stat.IOsTotalTicks,
+		&stat.WeightedIOTicks,
+		&stat.DiscardIOs,
+		&stat.DiscardMerges,
+		&stat.DiscardSectors,
+		&stat.DiscardTicks,
+	)
+	// An io.EOF error is ignored because it just means we read fewer than the full 15 fields.
+	if err == io.EOF {
+		return stat, count, nil
+	}
+	return stat, count, err
+}
diff --git a/diskstats.go b/diskstats.go
deleted file mode 100644
index a116cf3..0000000
--- a/diskstats.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2018 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"
-	"fmt"
-	"io"
-	"os"
-
-	"github.com/prometheus/procfs/iostats"
-)
-
-const (
-	diskstatsFilename = "diskstats"
-	statFormat        = "%d %d %s %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"
-)
-
-// NewDiskstats reads the diskstats file and returns
-// an array of Diskstats (one per line/device)
-func NewDiskstats() ([]iostats.IODeviceStats, error) {
-	fs, err := NewFS(DefaultMountPoint)
-	if err != nil {
-		return nil, err
-	}
-
-	return fs.NewDiskstats()
-}
-
-// NewDiskstats reads the diskstats file and returns
-// an array of Diskstats (one per line/device)
-func (fs FS) NewDiskstats() ([]iostats.IODeviceStats, error) {
-	file, err := os.Open(fs.Path(diskstatsFilename))
-	if err != nil {
-		return nil, err
-	}
-	defer file.Close()
-
-	diskstats := []iostats.IODeviceStats{}
-	scanner := bufio.NewScanner(file)
-	for scanner.Scan() {
-		d := &iostats.IODeviceStats{}
-		count, err := fmt.Sscanf(scanner.Text(), statFormat,
-			&d.MajorNumber,
-			&d.MinorNumber,
-			&d.DeviceName,
-			&d.ReadIOs,
-			&d.ReadMerges,
-			&d.ReadSectors,
-			&d.ReadTicks,
-			&d.WriteIOs,
-			&d.WriteMerges,
-			&d.WriteSectors,
-			&d.WriteTicks,
-			&d.IOsInProgress,
-			&d.IOsTotalTicks,
-			&d.WeightedIOTicks,
-			&d.DiscardIOs,
-			&d.DiscardMerges,
-			&d.DiscardSectors,
-			&d.DiscardTicks)
-		if err != nil && err != io.EOF {
-			return diskstats, err
-		}
-		if count == 14 || count == 18 {
-			diskstats = append(diskstats, *d)
-		}
-	}
-	return diskstats, nil
-}
diff --git a/fs.go b/fs.go
index b6c6b2c..f7a151c 100644
--- a/fs.go
+++ b/fs.go
@@ -17,9 +17,6 @@ import (
 	"fmt"
 	"os"
 	"path"
-
-	"github.com/prometheus/procfs/nfs"
-	"github.com/prometheus/procfs/xfs"
 )
 
 // FS represents the pseudo-filesystem proc, which provides an interface to
@@ -47,36 +44,3 @@ func NewFS(mountPoint string) (FS, error) {
 func (fs FS) Path(p ...string) string {
 	return path.Join(append([]string{string(fs)}, p...)...)
 }
-
-// XFSStats retrieves XFS filesystem runtime statistics.
-func (fs FS) XFSStats() (*xfs.Stats, error) {
-	f, err := os.Open(fs.Path("fs/xfs/stat"))
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	return xfs.ParseStats(f)
-}
-
-// NFSClientRPCStats retrieves NFS client RPC statistics.
-func (fs FS) NFSClientRPCStats() (*nfs.ClientRPCStats, error) {
-	f, err := os.Open(fs.Path("net/rpc/nfs"))
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	return nfs.ParseClientRPCStats(f)
-}
-
-// NFSdServerRPCStats retrieves NFS daemon RPC statistics.
-func (fs FS) NFSdServerRPCStats() (*nfs.ServerRPCStats, error) {
-	f, err := os.Open(fs.Path("net/rpc/nfsd"))
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	return nfs.ParseServerRPCStats(f)
-}
diff --git a/iostats/iostats.go b/iostats/iostats.go
deleted file mode 100644
index 97e8314..0000000
--- a/iostats/iostats.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2018 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 iostats
-
-// IODevice contains identifying information for an I/O device
-type IODevice struct {
-	MajorNumber uint32
-	MinorNumber uint32
-	DeviceName  string
-}
-
-// IOStats models the iostats data described in the kernel documentation
-// https://www.kernel.org/doc/Documentation/iostats.txt,
-// https://www.kernel.org/doc/Documentation/block/stat.txt,
-// and https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
-type IOStats struct {
-	// ReadIOs is the number of reads completed successfully.
-	ReadIOs uint64
-	// ReadMerges is the number of reads merged.  Reads and writes
-	// which are adjacent to each other may be merged for efficiency.
-	ReadMerges uint64
-	// ReadSectors is the total number of sectors read successfully.
-	ReadSectors uint64
-	// ReadTicks is the total number of milliseconds spent by all reads.
-	ReadTicks uint64
-	// WriteIOs is the total number of writes completed successfully.
-	WriteIOs uint64
-	// WriteMerges is the number of reads merged.
-	WriteMerges uint64
-	// WriteSectors is the total number of sectors written successfully.
-	WriteSectors uint64
-	// WriteTicks is the total number of milliseconds spent by all writes.
-	WriteTicks uint64
-	// IOsInProgress is number of I/Os currently in progress.
-	IOsInProgress uint64
-	// IOsTotalTicks is the number of milliseconds spent doing I/Os.
-	// This field increases so long as IosInProgress is nonzero.
-	IOsTotalTicks uint64
-	// WeightedIOTicks is the weighted number of milliseconds spent doing I/Os.
-	// This can also be used to estimate average queue wait time for requests.
-	WeightedIOTicks uint64
-	// DiscardIOs is the total number of discards completed successfully.
-	DiscardIOs uint64
-	// DiscardMerges is the number of discards merged.
-	DiscardMerges uint64
-	// DiscardSectors is the total number of sectors discarded successfully.
-	DiscardSectors uint64
-	// DiscardTicks is the total number of milliseconds spent by all discards.
-	DiscardTicks uint64
-}
-
-// IODeviceStats combines IODevice and IOStats
-type IODeviceStats struct {
-	IODevice
-	IOStats
-}
diff --git a/nfs/nfs.go b/nfs/nfs.go
index 651bf68..4c8dad4 100644
--- a/nfs/nfs.go
+++ b/nfs/nfs.go
@@ -15,6 +15,11 @@
 // Fields are documented in https://www.svennd.be/nfsd-stats-explained-procnetrpcnfsd/
 package nfs
 
+import (
+	"os"
+	"path"
+)
+
 // ReplyCache models the "rc" line.
 type ReplyCache struct {
 	Hits    uint64
@@ -261,3 +266,27 @@ type ServerRPCStats struct {
 	ServerV4Stats  ServerV4Stats
 	V4Ops          V4Ops
 }
+
+// ReadClientRPCStats retrieves NFS client RPC statistics
+// from proc/net/rpc/nfs.
+func ReadClientRPCStats(procfs string) (*ClientRPCStats, error) {
+	f, err := os.Open(path.Join(procfs, "net/rpc/nfs"))
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	return ParseClientRPCStats(f)
+}
+
+// ReadServerRPCStats retrieves NFS daemon RPC statistics
+// from proc/net/rpc/nfsd.
+func ReadServerRPCStats(procfs string) (*ServerRPCStats, error) {
+	f, err := os.Open(path.Join(procfs, "net/rpc/nfsd"))
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	return ParseServerRPCStats(f)
+}
diff --git a/sysfs/block_device.go b/sysfs/block_device.go
deleted file mode 100644
index 6939529..0000000
--- a/sysfs/block_device.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2018 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 !windows
-
-package sysfs
-
-import (
-	"fmt"
-	"io/ioutil"
-	"strings"
-
-	"github.com/prometheus/procfs/iostats"
-)
-
-const (
-	blockPath             = "block"
-	blockDeviceStatFormat = "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"
-)
-
-// BlockDevice represents block device in the sys filesystem
-// Docs here: https://www.kernel.org/doc/Documentation/block/
-type BlockDevice struct {
-	DeviceName string
-	fs         FS
-}
-
-// AllBlockDevices gets the list of block devices from the sys file system
-func (fs FS) AllBlockDevices() ([]BlockDevice, error) {
-	deviceDirs, err := ioutil.ReadDir(fs.Path(blockPath))
-	if err != nil {
-		return nil, err
-	}
-	devices := []BlockDevice{}
-	for _, deviceDir := range deviceDirs {
-		if deviceDir.IsDir() {
-			devices = append(devices, BlockDevice{deviceDir.Name(), fs})
-		}
-	}
-	return devices, nil
-}
-
-// NewBlockDeviceStat returns stats for the block device read from /sys/block/<device>/stat.
-func (d BlockDevice) NewBlockDeviceStat() (iostats.IOStats, error) {
-	stat := iostats.IOStats{}
-	bytes, err := ioutil.ReadFile(d.fs.Path(blockPath, d.DeviceName, "stat"))
-	if err != nil {
-		return stat, err
-	}
-	count, err := fmt.Sscanf(strings.TrimSpace(string(bytes)), blockDeviceStatFormat,
-		&stat.ReadIOs,
-		&stat.ReadMerges,
-		&stat.ReadSectors,
-		&stat.ReadTicks,
-		&stat.WriteIOs,
-		&stat.WriteMerges,
-		&stat.WriteSectors,
-		&stat.WriteTicks,
-		&stat.IOsInProgress,
-		&stat.IOsTotalTicks,
-		&stat.WeightedIOTicks,
-		&stat.DiscardIOs,
-		&stat.DiscardMerges,
-		&stat.DiscardSectors,
-		&stat.DiscardTicks,
-	)
-	if count == 11 || count == 15 {
-		return stat, nil
-	}
-	return iostats.IOStats{}, err
-}
diff --git a/sysfs/fs.go b/sysfs/fs.go
index fb15d43..8dd258d 100644
--- a/sysfs/fs.go
+++ b/sysfs/fs.go
@@ -17,9 +17,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-
-	"github.com/prometheus/procfs/bcache"
-	"github.com/prometheus/procfs/xfs"
 )
 
 // FS represents the pseudo-filesystem sys, which provides an interface to
@@ -47,62 +44,3 @@ func NewFS(mountPoint string) (FS, error) {
 func (fs FS) Path(p ...string) string {
 	return filepath.Join(append([]string{string(fs)}, p...)...)
 }
-
-// XFSStats retrieves XFS filesystem runtime statistics for each mounted XFS
-// filesystem.  Only available on kernel 4.4+.  On older kernels, an empty
-// slice of *xfs.Stats will be returned.
-func (fs FS) XFSStats() ([]*xfs.Stats, error) {
-	matches, err := filepath.Glob(fs.Path("fs/xfs/*/stats/stats"))
-	if err != nil {
-		return nil, err
-	}
-
-	stats := make([]*xfs.Stats, 0, len(matches))
-	for _, m := range matches {
-		f, err := os.Open(m)
-		if err != nil {
-			return nil, err
-		}
-
-		// "*" used in glob above indicates the name of the filesystem.
-		name := filepath.Base(filepath.Dir(filepath.Dir(m)))
-
-		// File must be closed after parsing, regardless of success or
-		// failure.  Defer is not used because of the loop.
-		s, err := xfs.ParseStats(f)
-		_ = f.Close()
-		if err != nil {
-			return nil, err
-		}
-
-		s.Name = name
-		stats = append(stats, s)
-	}
-
-	return stats, nil
-}
-
-// BcacheStats retrieves bcache runtime statistics for each bcache.
-func (fs FS) BcacheStats() ([]*bcache.Stats, error) {
-	matches, err := filepath.Glob(fs.Path("fs/bcache/*-*"))
-	if err != nil {
-		return nil, err
-	}
-
-	stats := make([]*bcache.Stats, 0, len(matches))
-	for _, uuidPath := range matches {
-		// "*-*" in glob above indicates the name of the bcache.
-		name := filepath.Base(uuidPath)
-
-		// stats
-		s, err := bcache.GetStats(uuidPath)
-		if err != nil {
-			return nil, err
-		}
-
-		s.Name = name
-		stats = append(stats, s)
-	}
-
-	return stats, nil
-}
diff --git a/xfs/xfs.go b/xfs/xfs.go
index d86794b..4bb793f 100644
--- a/xfs/xfs.go
+++ b/xfs/xfs.go
@@ -14,6 +14,12 @@
 // Package xfs provides access to statistics exposed by the XFS filesystem.
 package xfs
 
+import (
+	"os"
+	"path"
+	"path/filepath"
+)
+
 // Stats contains XFS filesystem runtime statistics, parsed from
 // /proc/fs/xfs/stat.
 //
@@ -161,3 +167,49 @@ type ExtendedPrecisionStats struct {
 	WriteBytes uint64
 	ReadBytes  uint64
 }
+
+// ReadProcStat retrieves XFS filesystem runtime statistics
+// from proc/fs/xfs/stat given the profs mount point.
+func ReadProcStat(procfs string) (*Stats, error) {
+	f, err := os.Open(path.Join(procfs, "fs/xfs/stat"))
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	return ParseStats(f)
+}
+
+// ReadSysStats retrieves XFS filesystem runtime statistics for each mounted XFS
+// filesystem.  Only available on kernel 4.4+.  On older kernels, an empty
+// slice of *xfs.Stats will be returned.
+func ReadSysStats(sysfs string) ([]*Stats, error) {
+	matches, err := filepath.Glob(path.Join(sysfs, "fs/xfs/*/stats/stats"))
+	if err != nil {
+		return nil, err
+	}
+
+	stats := make([]*Stats, 0, len(matches))
+	for _, m := range matches {
+		f, err := os.Open(m)
+		if err != nil {
+			return nil, err
+		}
+
+		// "*" used in glob above indicates the name of the filesystem.
+		name := filepath.Base(filepath.Dir(filepath.Dir(m)))
+
+		// File must be closed after parsing, regardless of success or
+		// failure.  Defer is not used because of the loop.
+		s, err := ParseStats(f)
+		_ = f.Close()
+		if err != nil {
+			return nil, err
+		}
+
+		s.Name = name
+		stats = append(stats, s)
+	}
+
+	return stats, nil
+}


Test patch:
diff --git a/bcache/get_test.go b/bcache/get_test.go
index 1d41a5a..5bea2f7 100644
--- a/bcache/get_test.go
+++ b/bcache/get_test.go
@@ -18,6 +18,48 @@ import (
 	"testing"
 )
 
+func TestFSBcacheStats(t *testing.T) {
+	stats, err := ReadStats("../fixtures/sys")
+	if err != nil {
+		t.Fatalf("failed to parse bcache stats: %v", err)
+	}
+
+	tests := []struct {
+		name   string
+		bdevs  int
+		caches int
+	}{
+		{
+			name:   "deaddd54-c735-46d5-868e-f331c5fd7c74",
+			bdevs:  1,
+			caches: 1,
+		},
+	}
+
+	const expect = 1
+
+	if l := len(stats); l != expect {
+		t.Fatalf("unexpected number of bcache stats: %d", l)
+	}
+	if l := len(tests); l != expect {
+		t.Fatalf("unexpected number of tests: %d", l)
+	}
+
+	for i, tt := range tests {
+		if want, got := tt.name, stats[i].Name; want != got {
+			t.Errorf("unexpected stats name:\nwant: %q\nhave: %q", want, got)
+		}
+
+		if want, got := tt.bdevs, len(stats[i].Bdevs); want != got {
+			t.Errorf("unexpected value allocated:\nwant: %d\nhave: %d", want, got)
+		}
+
+		if want, got := tt.caches, len(stats[i].Caches); want != got {
+			t.Errorf("unexpected value allocated:\nwant: %d\nhave: %d", want, got)
+		}
+	}
+}
+
 func TestDehumanizeTests(t *testing.T) {
 	dehumanizeTests := []struct {
 		in      []byte
diff --git a/blockdevice/stats_test.go b/blockdevice/stats_test.go
new file mode 100644
index 0000000..e5a7b8c
--- /dev/null
+++ b/blockdevice/stats_test.go
@@ -0,0 +1,90 @@
+// Copyright 2018 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 blockdevice
+
+import (
+	"testing"
+)
+
+const (
+	failMsgFormat  = "%v, expected %v, actual %v"
+	procfsFixtures = "../fixtures/proc"
+	sysfsFixtures  = "../fixtures/sys"
+)
+
+func TestDiskstats(t *testing.T) {
+	diskstats, err := ReadProcDiskstats(procfsFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expectedNumOfDevices := 49
+	if len(diskstats) != expectedNumOfDevices {
+		t.Errorf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(diskstats))
+	}
+	if diskstats[0].DeviceName != "ram0" {
+		t.Errorf(failMsgFormat, "Incorrect device name", "ram0", diskstats[0].DeviceName)
+	}
+	if diskstats[1].IoStatsCount != 14 {
+		t.Errorf(failMsgFormat, "Incorrect number of stats read", 14, diskstats[0].IoStatsCount)
+	}
+	if diskstats[24].WriteIOs != 28444756 {
+		t.Errorf(failMsgFormat, "Incorrect writes completed", 28444756, diskstats[24].WriteIOs)
+	}
+	if diskstats[48].DiscardTicks != 11130 {
+		t.Errorf(failMsgFormat, "Incorrect discard time", 11130, diskstats[48].DiscardTicks)
+	}
+	if diskstats[48].IoStatsCount != 18 {
+		t.Errorf(failMsgFormat, "Incorrect number of stats read", 18, diskstats[48].IoStatsCount)
+	}
+}
+
+func TestBlockDevice(t *testing.T) {
+	devices, err := ListSysBlockDevices(sysfsFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expectedNumOfDevices := 2
+	if len(devices) != expectedNumOfDevices {
+		t.Fatalf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(devices))
+	}
+	if devices[0] != "dm-0" {
+		t.Errorf(failMsgFormat, "Incorrect device name", "dm-0", devices[0])
+	}
+	device0stats, count, err := ReadSysBlockDeviceStat(sysfsFixtures, devices[0])
+	if err != nil {
+		t.Fatal(err)
+	}
+	if count != 11 {
+		t.Errorf(failMsgFormat, "Incorrect number of stats read", 11, count)
+	}
+	if device0stats.ReadIOs != 6447303 {
+		t.Errorf(failMsgFormat, "Incorrect read I/Os", 6447303, device0stats.ReadIOs)
+	}
+	if device0stats.WeightedIOTicks != 6088971 {
+		t.Errorf(failMsgFormat, "Incorrect time in queue", 6088971, device0stats.WeightedIOTicks)
+	}
+	device1stats, count, err := ReadSysBlockDeviceStat(sysfsFixtures, devices[1])
+	if count != 15 {
+		t.Errorf(failMsgFormat, "Incorrect number of stats read", 15, count)
+	}
+	if err != nil {
+		t.Fatal(err)
+	}
+	if device1stats.WriteSectors != 286915323 {
+		t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device1stats.WriteSectors)
+	}
+	if device1stats.DiscardTicks != 12 {
+		t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device1stats.DiscardTicks)
+	}
+}
diff --git a/diskstats_test.go b/diskstats_test.go
deleted file mode 100644
index b9608fd..0000000
--- a/diskstats_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2018 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 (
-	"testing"
-)
-
-const (
-	failMsgFormat        = "%v, expected %v, actual %v"
-	expectedNumOfDevices = 49
-)
-
-func TestDiskstats(t *testing.T) {
-	diskstats, err := FS(procTestFixtures).NewDiskstats()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(diskstats) != expectedNumOfDevices {
-		t.Errorf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(diskstats))
-	}
-	if diskstats[0].DeviceName != "ram0" {
-		t.Errorf(failMsgFormat, "Incorrect device name", "ram0", diskstats[0].DeviceName)
-	}
-	if diskstats[24].WriteIOs != 28444756 {
-		t.Errorf(failMsgFormat, "Incorrect writes completed", 28444756, diskstats[24].WriteIOs)
-	}
-	if diskstats[48].DiscardTicks != 11130 {
-		t.Errorf(failMsgFormat, "Incorrect discard time", 11130, diskstats[48].DiscardTicks)
-	}
-}
diff --git a/fs_test.go b/fs_test.go
index 0927ab9..ce16fae 100644
--- a/fs_test.go
+++ b/fs_test.go
@@ -28,16 +28,3 @@ func TestNewFS(t *testing.T) {
 		t.Error("want NewFS to fail if mount point is not a directory")
 	}
 }
-
-func TestFSXFSStats(t *testing.T) {
-	stats, err := FS(procTestFixtures).XFSStats()
-	if err != nil {
-		t.Fatalf("failed to parse XFS stats: %v", err)
-	}
-
-	// Very lightweight test just to sanity check the path used
-	// to open XFS stats. Heavier tests in package xfs.
-	if want, got := uint32(92447), stats.ExtentAllocation.ExtentsAllocated; want != got {
-		t.Errorf("unexpected extents allocated:\nwant: %d\nhave: %d", want, got)
-	}
-}
diff --git a/sysfs/block_device_test.go b/sysfs/block_device_test.go
deleted file mode 100644
index c24a804..0000000
--- a/sysfs/block_device_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2018 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 !windows
-
-package sysfs
-
-import (
-	"testing"
-)
-
-const (
-	failMsgFormat        = "%v, expected %v, actual %v"
-	expectedNumOfDevices = 2
-)
-
-func TestBlockDevice(t *testing.T) {
-	devices, err := FS(sysTestFixtures).AllBlockDevices()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if len(devices) != expectedNumOfDevices {
-		t.Fatalf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(devices))
-	}
-	if devices[0].DeviceName != "dm-0" {
-		t.Errorf(failMsgFormat, "Incorrect device name", "dm-0", devices[0].DeviceName)
-	}
-	device0stats, err := devices[0].NewBlockDeviceStat()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if device0stats.ReadIOs != 6447303 {
-		t.Errorf(failMsgFormat, "Incorrect read I/Os", 6447303, device0stats.ReadIOs)
-	}
-	if device0stats.WeightedIOTicks != 6088971 {
-		t.Errorf(failMsgFormat, "Incorrect time in queue", 6088971, device0stats.WeightedIOTicks)
-	}
-	device1stats, err := devices[1].NewBlockDeviceStat()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if device1stats.WriteSectors != 286915323 {
-		t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device1stats.WriteSectors)
-	}
-	if device1stats.DiscardTicks != 12 {
-		t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device1stats.DiscardTicks)
-	}
-}
diff --git a/sysfs/fs_test.go b/sysfs/fs_test.go
index 33a1741..2ef5a73 100644
--- a/sysfs/fs_test.go
+++ b/sysfs/fs_test.go
@@ -27,86 +27,8 @@ func TestNewFS(t *testing.T) {
 	if _, err := NewFS("doc.go"); err == nil {
 		t.Error("want NewFS to fail if mount point is not a directory")
 	}
-}
-
-func TestFSXFSStats(t *testing.T) {
-	stats, err := FS(sysTestFixtures).XFSStats()
-	if err != nil {
-		t.Fatalf("failed to parse XFS stats: %v", err)
-	}
-
-	tests := []struct {
-		name      string
-		allocated uint32
-	}{
-		{
-			name:      "sda1",
-			allocated: 1,
-		},
-		{
-			name:      "sdb1",
-			allocated: 2,
-		},
-	}
-
-	const expect = 2
-
-	if l := len(stats); l != expect {
-		t.Fatalf("unexpected number of XFS stats: %d", l)
-	}
-	if l := len(tests); l != expect {
-		t.Fatalf("unexpected number of tests: %d", l)
-	}
-
-	for i, tt := range tests {
-		if want, got := tt.name, stats[i].Name; want != got {
-			t.Errorf("unexpected stats name:\nwant: %q\nhave: %q", want, got)
-		}
-
-		if want, got := tt.allocated, stats[i].ExtentAllocation.ExtentsAllocated; want != got {
-			t.Errorf("unexpected extents allocated:\nwant: %d\nhave: %d", want, got)
-		}
-	}
-}
-
-func TestFSBcacheStats(t *testing.T) {
-	stats, err := FS(sysTestFixtures).BcacheStats()
-	if err != nil {
-		t.Fatalf("failed to parse bcache stats: %v", err)
-	}
-
-	tests := []struct {
-		name   string
-		bdevs  int
-		caches int
-	}{
-		{
-			name:   "deaddd54-c735-46d5-868e-f331c5fd7c74",
-			bdevs:  1,
-			caches: 1,
-		},
-	}
-
-	const expect = 1
-
-	if l := len(stats); l != expect {
-		t.Fatalf("unexpected number of bcache stats: %d", l)
-	}
-	if l := len(tests); l != expect {
-		t.Fatalf("unexpected number of tests: %d", l)
-	}
-
-	for i, tt := range tests {
-		if want, got := tt.name, stats[i].Name; want != got {
-			t.Errorf("unexpected stats name:\nwant: %q\nhave: %q", want, got)
-		}
-
-		if want, got := tt.bdevs, len(stats[i].Bdevs); want != got {
-			t.Errorf("unexpected value allocated:\nwant: %d\nhave: %d", want, got)
-		}
 
-		if want, got := tt.caches, len(stats[i].Caches); want != got {
-			t.Errorf("unexpected value allocated:\nwant: %d\nhave: %d", want, got)
-		}
+	if _, err := NewFS(sysTestFixtures); err != nil {
+		t.Error("want NewFS to succeed if mount point exists")
 	}
 }
diff --git a/xfs/parse_test.go b/xfs/parse_test.go
index cd4dcbb..e2208f7 100644
--- a/xfs/parse_test.go
+++ b/xfs/parse_test.go
@@ -18,7 +18,6 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/prometheus/procfs"
 	"github.com/prometheus/procfs/xfs"
 )
 
@@ -425,7 +424,7 @@ func TestParseStats(t *testing.T) {
 			stats, err = xfs.ParseStats(strings.NewReader(tt.s))
 		}
 		if tt.fs {
-			stats, err = procfs.FS("../fixtures/proc").XFSStats()
+			stats, err = xfs.ReadProcStat("../fixtures/proc")
 		}
 
 		if tt.invalid && err == nil {
diff --git a/xfs/xfs_test.go b/xfs/xfs_test.go
new file mode 100644
index 0000000..d6f1fcb
--- /dev/null
+++ b/xfs/xfs_test.go
@@ -0,0 +1,74 @@
+// Copyright 2019 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 xfs provides access to statistics exposed by the XFS filesystem.
+package xfs_test
+
+import (
+	"testing"
+
+	"github.com/prometheus/procfs/xfs"
+)
+
+func TestReadProcStat(t *testing.T) {
+	stats, err := xfs.ReadProcStat("../fixtures/proc")
+	if err != nil {
+		t.Fatalf("failed to parse XFS stats: %v", err)
+	}
+
+	// Very lightweight test just to sanity check the path used
+	// to open XFS stats. Heavier tests in package xfs.
+	if want, got := uint32(92447), stats.ExtentAllocation.ExtentsAllocated; want != got {
+		t.Errorf("unexpected extents allocated:\nwant: %d\nhave: %d", want, got)
+	}
+}
+
+func TestReadSysStats(t *testing.T) {
+	stats, err := xfs.ReadSysStats("../fixtures/sys")
+	if err != nil {
+		t.Fatalf("failed to parse XFS stats: %v", err)
+	}
+
+	tests := []struct {
+		name      string
+		allocated uint32
+	}{
+		{
+			name:      "sda1",
+			allocated: 1,
+		},
+		{
+			name:      "sdb1",
+			allocated: 2,
+		},
+	}
+
+	const expect = 2
+
+	if l := len(stats); l != expect {
+		t.Fatalf("unexpected number of XFS stats: %d", l)
+	}
+	if l := len(tests); l != expect {
+		t.Fatalf("unexpected number of tests: %d", l)
+	}
+
+	for i, tt := range tests {
+		if want, got := tt.name, stats[i].Name; want != got {
+			t.Errorf("unexpected stats name:\nwant: %q\nhave: %q", want, got)
+		}
+
+		if want, got := tt.allocated, stats[i].ExtentAllocation.ExtentsAllocated; want != got {
+			t.Errorf("unexpected extents allocated:\nwant: %d\nhave: %d", want, got)
+		}
+	}
+}