Repository: prometheus/procfs
Pull request #246 (2019-12-12 17:26:45)

Add support for /proc/swaps
This code (both implementation and tests) is largely based on the existing mountinfo implementation.

Implementation of #244.

stevenjm: @mdlayher Does this look reasonable to you?

Patch:
diff --git a/Makefile.common b/Makefile.common
index d7aea1b..066866e 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -69,12 +69,12 @@ else
 	GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)
 endif
 
-PROMU_VERSION ?= 0.4.0
+PROMU_VERSION ?= 0.5.0
 PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
 
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.16.0
+GOLANGCI_LINT_VERSION ?= v1.18.0
 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
@@ -86,7 +86,8 @@ endif
 PREFIX                  ?= $(shell pwd)
 BIN_DIR                 ?= $(shell pwd)
 DOCKER_IMAGE_TAG        ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
-DOCKERFILE_PATH         ?= ./
+DOCKERFILE_PATH         ?= ./Dockerfile
+DOCKERBUILD_CONTEXT     ?= ./
 DOCKER_REPO             ?= prom
 
 DOCKER_ARCHS            ?= amd64
@@ -200,7 +201,7 @@ endif
 .PHONY: common-build
 common-build: promu
 	@echo ">> building binaries"
-	GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX)
+	GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES)
 
 .PHONY: common-tarball
 common-tarball: promu
@@ -211,9 +212,10 @@ common-tarball: promu
 common-docker: $(BUILD_DOCKER_ARCHS)
 $(BUILD_DOCKER_ARCHS): common-docker-%:
 	docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(DOCKER_IMAGE_TAG)" \
+		-f $(DOCKERFILE_PATH) \
 		--build-arg ARCH="$*" \
 		--build-arg OS="linux" \
-		$(DOCKERFILE_PATH)
+		$(DOCKERBUILD_CONTEXT)
 
 .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
 common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
diff --git a/fixtures.ttar b/fixtures.ttar
index e4c17a2..eeb3d00 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -1952,6 +1952,12 @@ procs_blocked 1
 softirq 5057579 250191 1481983 1647 211099 186066 0 1783454 622196 12499 508444
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/swaps
+Lines: 2
+Filename				Type		Size	Used	Priority
+/dev/dm-2                               partition	131068	176	-2
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/proc/symlinktargets
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/net_softnet.go b/net_softnet.go
index 6fcad20..2a65cf1 100644
--- a/net_softnet.go
+++ b/net_softnet.go
@@ -14,78 +14,85 @@
 package procfs
 
 import (
+	"bufio"
+	"bytes"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"strconv"
 	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
 )
 
 // For the proc file format details,
 // see https://elixir.bootlin.com/linux/v4.17/source/net/core/net-procfs.c#L162
 // and https://elixir.bootlin.com/linux/v4.17/source/include/linux/netdevice.h#L2810.
 
-// SoftnetEntry contains a single row of data from /proc/net/softnet_stat
-type SoftnetEntry struct {
+// SoftnetStat contains a single row of data from /proc/net/softnet_stat
+type SoftnetStat struct {
 	// Number of processed packets
-	Processed uint
+	Processed uint32
 	// Number of dropped packets
-	Dropped uint
+	Dropped uint32
 	// Number of times processing packets ran out of quota
-	TimeSqueezed uint
+	TimeSqueezed uint32
 }
 
-// GatherSoftnetStats reads /proc/net/softnet_stat, parse the relevant columns,
-// and then return a slice of SoftnetEntry's.
-func (fs FS) GatherSoftnetStats() ([]SoftnetEntry, error) {
-	data, err := ioutil.ReadFile(fs.proc.Path("net/softnet_stat"))
+// NetSoftnetStat reads data from /proc/net/softnet_stat.
+func (fs FS) NetSoftnetStat() ([]SoftnetStat, error) {
+	b, err := util.ReadFileNoStat(fs.proc.Path("net/softnet_stat"))
 	if err != nil {
-		return nil, fmt.Errorf("error reading softnet %s: %s", fs.proc.Path("net/softnet_stat"), err)
+		return nil, err
 	}
 
-	return parseSoftnetEntries(data)
+	entries, err := parseSoftnet(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse /proc/net/softnet_stat: %v", err)
+	}
+
+	return entries, nil
 }
 
-func parseSoftnetEntries(data []byte) ([]SoftnetEntry, error) {
-	lines := strings.Split(string(data), "\n")
-	entries := make([]SoftnetEntry, 0)
-	var err error
-	const (
-		expectedColumns = 11
-	)
-	for _, line := range lines {
-		columns := strings.Fields(line)
+func parseSoftnet(r io.Reader) ([]SoftnetStat, error) {
+	const expectedColumns = 11
+
+	s := bufio.NewScanner(r)
+
+	var stats []SoftnetStat
+	for s.Scan() {
+		columns := strings.Fields(s.Text())
 		width := len(columns)
-		if width == 0 {
-			continue
-		}
-		if width != expectedColumns {
-			return []SoftnetEntry{}, fmt.Errorf("%d columns were detected, but %d were expected", width, expectedColumns)
+
+		if width != 11 {
+			return nil, fmt.Errorf("%d columns were detected, but %d were expected", width, expectedColumns)
 		}
-		var entry SoftnetEntry
-		if entry, err = parseSoftnetEntry(columns); err != nil {
-			return []SoftnetEntry{}, err
+
+		// We only parse the first three columns at the moment.
+		us, err := parseHexUint32s(columns[0:3])
+		if err != nil {
+			return nil, err
 		}
-		entries = append(entries, entry)
+
+		stats = append(stats, SoftnetStat{
+			Processed:    us[0],
+			Dropped:      us[1],
+			TimeSqueezed: us[2],
+		})
 	}
 
-	return entries, nil
+	return stats, nil
 }
 
-func parseSoftnetEntry(columns []string) (SoftnetEntry, error) {
-	var err error
-	var processed, dropped, timeSqueezed uint64
-	if processed, err = strconv.ParseUint(columns[0], 16, 32); err != nil {
-		return SoftnetEntry{}, fmt.Errorf("Unable to parse column 0: %s", err)
-	}
-	if dropped, err = strconv.ParseUint(columns[1], 16, 32); err != nil {
-		return SoftnetEntry{}, fmt.Errorf("Unable to parse column 1: %s", err)
-	}
-	if timeSqueezed, err = strconv.ParseUint(columns[2], 16, 32); err != nil {
-		return SoftnetEntry{}, fmt.Errorf("Unable to parse column 2: %s", err)
+func parseHexUint32s(ss []string) ([]uint32, error) {
+	us := make([]uint32, 0, len(ss))
+	for _, s := range ss {
+		u, err := strconv.ParseUint(s, 16, 32)
+		if err != nil {
+			return nil, err
+		}
+
+		us = append(us, uint32(u))
 	}
-	return SoftnetEntry{
-		Processed:    uint(processed),
-		Dropped:      uint(dropped),
-		TimeSqueezed: uint(timeSqueezed),
-	}, nil
+
+	return us, nil
 }
diff --git a/swaps.go b/swaps.go
new file mode 100644
index 0000000..15edc22
--- /dev/null
+++ b/swaps.go
@@ -0,0 +1,89 @@
+// 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 procfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// Swap represents an entry in /proc/swaps.
+type Swap struct {
+	Filename string
+	Type     string
+	Size     int
+	Used     int
+	Priority int
+}
+
+// Swaps returns a slice of all configured swap devices on the system.
+func (fs FS) Swaps() ([]*Swap, error) {
+	data, err := util.ReadFileNoStat(fs.proc.Path("swaps"))
+	if err != nil {
+		return nil, err
+	}
+	return parseSwaps(data)
+}
+
+func parseSwaps(info []byte) ([]*Swap, error) {
+	swaps := []*Swap{}
+	scanner := bufio.NewScanner(bytes.NewReader(info))
+	scanner.Scan() // ignore header line
+	for scanner.Scan() {
+		swapString := scanner.Text()
+		parsedSwap, err := parseSwapString(swapString)
+		if err != nil {
+			return nil, err
+		}
+		swaps = append(swaps, parsedSwap)
+	}
+
+	err := scanner.Err()
+	return swaps, err
+}
+
+func parseSwapString(swapString string) (*Swap, error) {
+	var err error
+
+	swapFields := strings.Fields(swapString)
+	swapLength := len(swapFields)
+	if swapLength < 5 {
+		return nil, fmt.Errorf("too few fields in swap string: %s", swapString)
+	}
+
+	swap := &Swap{
+		Filename: swapFields[0],
+		Type:     swapFields[1],
+	}
+
+	swap.Size, err = strconv.Atoi(swapFields[2])
+	if err != nil {
+		return nil, fmt.Errorf("invalid swap size: %s", swapFields[2])
+	}
+	swap.Used, err = strconv.Atoi(swapFields[3])
+	if err != nil {
+		return nil, fmt.Errorf("invalid swap used: %s", swapFields[3])
+	}
+	swap.Priority, err = strconv.Atoi(swapFields[4])
+	if err != nil {
+		return nil, fmt.Errorf("invalid swap priority: %s", swapFields[4])
+	}
+
+	return swap, nil
+}


Test patch:
diff --git a/net_softnet_test.go b/net_softnet_test.go
index a2658a3..5a52131 100644
--- a/net_softnet_test.go
+++ b/net_softnet_test.go
@@ -15,28 +15,28 @@ package procfs
 
 import (
 	"testing"
+
+	"github.com/google/go-cmp/cmp"
 )
 
-func TestSoftnet(t *testing.T) {
+func TestNetSoftnet(t *testing.T) {
 	fs, err := NewFS(procTestFixtures)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	entries, err := fs.GatherSoftnetStats()
+	want := []SoftnetStat{{
+		Processed:    0x00015c73,
+		Dropped:      0x00020e76,
+		TimeSqueezed: 0xf0000769,
+	}}
+
+	got, err := fs.NetSoftnetStat()
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	if want, got := uint(0x00015c73), entries[0].Processed; want != got {
-		t.Errorf("want %08x, got %08x", want, got)
-	}
-
-	if want, got := uint(0x00020e76), entries[0].Dropped; want != got {
-		t.Errorf("want %08x, got %08x", want, got)
-	}
-
-	if want, got := uint(0xF0000769), entries[0].TimeSqueezed; want != got {
-		t.Errorf("want %08x, got %08x", want, got)
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("unexpected softnet stats(-want +got):\n%s", diff)
 	}
 }
diff --git a/swaps_test.go b/swaps_test.go
new file mode 100644
index 0000000..70db6ab
--- /dev/null
+++ b/swaps_test.go
@@ -0,0 +1,113 @@
+// 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 procfs
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestSwaps(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatalf("failed to open procfs: %v", err)
+	}
+
+	swaps, err := fs.Swaps()
+	if err != nil {
+		t.Fatalf("failed to get swaps: %v", err)
+	}
+
+	if len(swaps) != 1 {
+		t.Fatalf("expected 1 swap entry, got %d", len(swaps))
+	}
+	swap := swaps[0]
+
+	if swap.Filename != "/dev/dm-2" {
+		t.Errorf("expected swap.Filename /dev/dm-2, got %s", swap.Filename)
+	}
+	if swap.Type != "partition" {
+		t.Errorf("expected swap.Type partition, got %s", swap.Type)
+	}
+	if swap.Size != 131068 {
+		t.Errorf("expected swap.Size 131068, got %d", swap.Size)
+	}
+	if swap.Used != 176 {
+		t.Errorf("expected swap.Used 176, got %d", swap.Used)
+	}
+	if swap.Priority != -2 {
+		t.Errorf("expected swap.Priority -2, got %d", swap.Priority)
+	}
+}
+
+func TestParseSwapString(t *testing.T) {
+	tests := []struct {
+		name    string
+		s       string
+		swap    *Swap
+		invalid bool
+	}{
+		{
+			name:    "device-mapper volume",
+			s:       "/dev/dm-2                               partition       131068  1024    -2",
+			invalid: false,
+			swap: &Swap{
+				Filename: "/dev/dm-2",
+				Type:     "partition",
+				Size:     131068,
+				Used:     1024,
+				Priority: -2,
+			},
+		},
+		{
+			name:    "Swap file",
+			s:       "/foo                                    file            1048572 0       -3",
+			invalid: false,
+			swap: &Swap{
+				Filename: "/foo",
+				Type:     "file",
+				Size:     1048572,
+				Used:     0,
+				Priority: -3,
+			},
+		},
+		{
+			name:    "Invalid number",
+			s:       "/dev/sda2                               partition       hello   world   -2",
+			invalid: true,
+		},
+		{
+			name:    "Not enough fields",
+			s:       "/dev/dm-2                               partition       131068  1024",
+			invalid: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			swap, err := parseSwapString(tt.s)
+
+			if tt.invalid && err == nil {
+				t.Error("unexpected success")
+			}
+			if !tt.invalid && err != nil {
+				t.Errorf("unexpected error: %v", err)
+			}
+
+			if !reflect.DeepEqual(tt.swap, swap) {
+				t.Errorf("swap:\nwant:\n%+v\nhave:\n%+v", tt.swap, swap)
+			}
+		})
+	}
+}