Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dependencies from procfs and sysfs onto xfs, nfs, bcache, and iostats #136

Merged
merged 5 commits into from
Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions bcache/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ".")
Expand Down
42 changes: 42 additions & 0 deletions bcache/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
181 changes: 181 additions & 0 deletions blockdevice/stats.go
Original file line number Diff line number Diff line change
@@ -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
}
90 changes: 90 additions & 0 deletions blockdevice/stats_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading