Repository: prometheus/procfs
Pull request #249 (2019-12-14 03:41:51)

Expose xs_qm_dquot_unused XFS counter
Add support for exposing the xs_qm_dquot_unused XFS statistic which appears in
Linux 4.20 and later.

Signed-off-by: Steven Kreuzer <skreuzer@FreeBSD.org>


Patch:
diff --git a/net_conntrackstat.go b/net_conntrackstat.go
new file mode 100644
index 0000000..1e27c83
--- /dev/null
+++ b/net_conntrackstat.go
@@ -0,0 +1,153 @@
+// 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"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// A ConntrackStatEntry represents one line from net/stat/nf_conntrack
+// and contains netfilter conntrack statistics at one CPU core
+type ConntrackStatEntry struct {
+	Entries       uint64
+	Found         uint64
+	Invalid       uint64
+	Ignore        uint64
+	Insert        uint64
+	InsertFailed  uint64
+	Drop          uint64
+	EarlyDrop     uint64
+	SearchRestart uint64
+}
+
+// Retrieves netfilter's conntrack statistics, split by CPU cores
+func (fs FS) ConntrackStat() ([]ConntrackStatEntry, error) {
+	return readConntrackStat(fs.proc.Path("net", "stat", "nf_conntrack"))
+}
+
+// Parses a slice of ConntrackStatEntries from the given filepath
+func readConntrackStat(path string) ([]ConntrackStatEntry, error) {
+	// This file is small and can be read with one syscall.
+	b, err := util.ReadFileNoStat(path)
+	if err != nil {
+		// Do not wrap this error so the caller can detect os.IsNotExist and
+		// similar conditions.
+		return nil, err
+	}
+
+	stat, err := parseConntrackStat(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to read conntrack stats from %q: %v", path, err)
+	}
+
+	return stat, nil
+}
+
+// Reads the contents of a conntrack statistics file and parses a slice of ConntrackStatEntries
+func parseConntrackStat(r io.Reader) ([]ConntrackStatEntry, error) {
+	var entries []ConntrackStatEntry
+
+	scanner := bufio.NewScanner(r)
+	scanner.Scan()
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		conntrackEntry, err := parseConntrackStatEntry(fields)
+		if err != nil {
+			return nil, err
+		}
+		entries = append(entries, *conntrackEntry)
+	}
+
+	return entries, nil
+}
+
+// Parses a ConntrackStatEntry from given array of fields
+func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) {
+	if len(fields) != 17 {
+		return nil, fmt.Errorf("invalid conntrackstat entry, missing fields")
+	}
+	entry := &ConntrackStatEntry{}
+
+	entries, err := parseConntrackStatField(fields[0])
+	if err != nil {
+		return nil, err
+	}
+	entry.Entries = entries
+
+	found, err := parseConntrackStatField(fields[2])
+	if err != nil {
+		return nil, err
+	}
+	entry.Found = found
+
+	invalid, err := parseConntrackStatField(fields[4])
+	if err != nil {
+		return nil, err
+	}
+	entry.Invalid = invalid
+
+	ignore, err := parseConntrackStatField(fields[5])
+	if err != nil {
+		return nil, err
+	}
+	entry.Ignore = ignore
+
+	insert, err := parseConntrackStatField(fields[8])
+	if err != nil {
+		return nil, err
+	}
+	entry.Insert = insert
+
+	insertFailed, err := parseConntrackStatField(fields[9])
+	if err != nil {
+		return nil, err
+	}
+	entry.InsertFailed = insertFailed
+
+	drop, err := parseConntrackStatField(fields[10])
+	if err != nil {
+		return nil, err
+	}
+	entry.Drop = drop
+
+	earlyDrop, err := parseConntrackStatField(fields[11])
+	if err != nil {
+		return nil, err
+	}
+	entry.EarlyDrop = earlyDrop
+
+	searchRestart, err := parseConntrackStatField(fields[16])
+	if err != nil {
+		return nil, err
+	}
+	entry.SearchRestart = searchRestart
+
+	return entry, nil
+}
+
+// Parses a uint64 from given hex in string
+func parseConntrackStatField(field string) (uint64, error) {
+	val, err := strconv.ParseUint(field, 16, 64)
+	if err != nil {
+		return 0, fmt.Errorf("couldn't parse \"%s\" field: %s", field, err)
+	}
+	return val, err
+}
diff --git a/xfs/parse.go b/xfs/parse.go
index f3f30d7..ba3afea 100644
--- a/xfs/parse.go
+++ b/xfs/parse.go
@@ -381,11 +381,15 @@ func extendedPrecisionStats(us []uint64) (ExtendedPrecisionStats, error) {
 }
 
 func quotaManagerStats(us []uint32) (QuotaManagerStats, error) {
-	if l := len(us); l < 8 {
-		return QuotaManagerStats{}, fmt.Errorf("insufficient number of values for XFS quota stats expecting at least 8, got: %d", l)
+	// The "Unused" attribute first appears in Linux 4.20
+	// As a result either 8 or 9 elements may appear in this slice depending on
+	// the kernel version.
+	l := len(us)
+	if l != 8 && l != 9 {
+		return QuotaManagerStats{}, fmt.Errorf("incorrect number of values for XFS quota stats: %d", l)
 	}
 
-	return QuotaManagerStats{
+	s := QuotaManagerStats{
 		Reclaims:      us[0],
 		ReclaimMisses: us[1],
 		DquoteDups:    us[2],
@@ -394,7 +398,13 @@ func quotaManagerStats(us []uint32) (QuotaManagerStats, error) {
 		Wants:         us[5],
 		ShakeReclaims: us[6],
 		InactReclaims: us[7],
-	}, nil
+	}
+
+	if l > 8 {
+		s.Unused = us[8]
+	}
+
+	return s, nil
 }
 
 func debugStats(us []uint32) (DebugStats, error) {
diff --git a/xfs/xfs.go b/xfs/xfs.go
index 018a0b8..cc8b36b 100644
--- a/xfs/xfs.go
+++ b/xfs/xfs.go
@@ -202,6 +202,7 @@ type QuotaManagerStats struct {
 	Wants         uint32
 	ShakeReclaims uint32
 	InactReclaims uint32
+	Unused        uint32
 }
 
 // XstratStats contains statistics regarding bytes processed by the XFS daemon.


Test patch:
diff --git a/net_conntrackstat_test.go b/net_conntrackstat_test.go
new file mode 100644
index 0000000..18fc066
--- /dev/null
+++ b/net_conntrackstat_test.go
@@ -0,0 +1,85 @@
+// 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 (
+	"bytes"
+	"reflect"
+	"testing"
+)
+
+func TestParseConntrackStat(t *testing.T) {
+	var nfConntrackStat = []byte(`entries  searched found new invalid ignore delete delete_list insert insert_failed drop early_drop icmp_error  expect_new expect_create expect_delete search_restart
+00000021  00000000 00000000 00000000 00000003 0000588a 00000000 00000000 00000000 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
+00000021  00000000 00000000 00000000 00000002 000056a4 00000000 00000000 00000000 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000002
+00000021  00000000 00000000 00000000 00000001 000058d4 00000000 00000000 00000000 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000001
+00000021  00000000 00000000 00000000 0000002f 00005688 00000000 00000000 00000000 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000004
+`)
+	r := bytes.NewReader(nfConntrackStat)
+
+	have, err := parseConntrackStat(r)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := []ConntrackStatEntry{
+		ConntrackStatEntry{
+			Entries:       33,
+			Found:         0,
+			Invalid:       3,
+			Ignore:        22666,
+			Insert:        0,
+			InsertFailed:  0,
+			Drop:          0,
+			EarlyDrop:     0,
+			SearchRestart: 0,
+		},
+		ConntrackStatEntry{
+			Entries:       33,
+			Found:         0,
+			Invalid:       2,
+			Ignore:        22180,
+			Insert:        0,
+			InsertFailed:  0,
+			Drop:          0,
+			EarlyDrop:     0,
+			SearchRestart: 2,
+		},
+		ConntrackStatEntry{
+			Entries:       33,
+			Found:         0,
+			Invalid:       1,
+			Ignore:        22740,
+			Insert:        0,
+			InsertFailed:  0,
+			Drop:          0,
+			EarlyDrop:     0,
+			SearchRestart: 1,
+		},
+		ConntrackStatEntry{
+			Entries:       33,
+			Found:         0,
+			Invalid:       47,
+			Ignore:        22152,
+			Insert:        0,
+			InsertFailed:  0,
+			Drop:          0,
+			EarlyDrop:     0,
+			SearchRestart: 4,
+		},
+	}
+	if !reflect.DeepEqual(want, have) {
+		t.Errorf("want %v, have %v", want, have)
+	}
+}
diff --git a/xfs/parse_test.go b/xfs/parse_test.go
index 1774665..b799f4b 100644
--- a/xfs/parse_test.go
+++ b/xfs/parse_test.go
@@ -368,6 +368,11 @@ func TestParseStats(t *testing.T) {
 			s:       "qm 1 2 3 4 5 6 7",
 			invalid: true,
 		},
+		{
+			name:    "qm bad",
+			s:       "qm 1 2 3 4 5 6 7 8 9 10",
+			invalid: true,
+		},
 		{
 			name: "qm OK",
 			s:    "qm 1 2 3 4 5 6 7 8",
@@ -384,6 +389,23 @@ func TestParseStats(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "qm OK",
+			s:    "qm 1 2 3 4 5 6 7 8 9",
+			stats: &xfs.Stats{
+				QuotaManager: xfs.QuotaManagerStats{
+					Reclaims:      1,
+					ReclaimMisses: 2,
+					DquoteDups:    3,
+					CacheMisses:   4,
+					CacheHits:     5,
+					Wants:         6,
+					ShakeReclaims: 7,
+					InactReclaims: 8,
+					Unused:        9,
+				},
+			},
+		},
 		{
 			name:    "abtb2 bad",
 			s:       "abtb2 1 2 3 4 5 6",