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
While implementing parsing of /proc/[pid]/mountstats, this blog was used heavily as a reference: https://utcc.utoronto.ca/~cks/space/blog/linux/NFSMountstatsIndex Special thanks to Chris Siebenmann for all of his posts explaining the various statistics available for NFS.

import (
	
	
	
	
	
	
)
Constants shared between multiple functions.
A Mount is a device mount parsed from /proc/[pid]/mountstats.
Name of the device.
The mount point of the device.
The filesystem type used by the device.
If available additional statistics related to this Mount. Use a type assertion to determine if additional statistics are available.
A MountStats is a type which contains detailed statistics for a specific type of Mount.
type MountStats interface {
	mountStats()
}
A MountStatsNFS is a MountStats implementation for NFSv3 and v4 mounts.
The version of statistics provided.
The mount options of the NFS mount.
The age of the NFS mount.
Statistics related to byte counters for various operations.
Statistics related to various NFS event occurrences.
Statistics broken down by filesystem operation.
Statistics about the NFS RPC transport.
mountStats implements MountStats.
func ( MountStatsNFS) () {}
A NFSBytesStats contains statistics about the number of bytes read and written by an NFS client to and from an NFS server.
Number of bytes read using the read() syscall.
Number of bytes written using the write() syscall.
Number of bytes read using the read() syscall in O_DIRECT mode.
Number of bytes written using the write() syscall in O_DIRECT mode.
Number of bytes read from the NFS server, in total.
Number of bytes written to the NFS server, in total.
Number of pages read directly via mmap()'d files.
Number of pages written directly via mmap()'d files.
A NFSEventsStats contains statistics about NFS event occurrences.
Number of times cached inode attributes are re-validated from the server.
Number of times cached dentry nodes are re-validated from the server.
Number of times an inode cache is cleared.
Number of times cached inode attributes are invalidated.
Number of times files or directories have been open()'d.
Number of times a directory lookup has occurred.
Number of times permissions have been checked.
Number of updates (and potential writes) to pages.
Number of pages read directly via mmap()'d files.
Number of times a group of pages have been read.
Number of pages written directly via mmap()'d files.
Number of times a group of pages have been written.
Number of times directory entries have been read with getdents().
Number of times attributes have been set on inodes.
Number of pending writes that have been forcefully flushed to the server.
Number of times fsync() has been called on directories and files.
Number of times locking has been attempted on a file.
Number of times files have been closed and released.
Unknown. Possibly unused.
Number of times files have been truncated.
Number of times a file has been grown due to writes beyond its existing end.
Number of times a file was removed while still open by another process.
Number of times the NFS server gave less data than expected while reading.
Number of times the NFS server wrote less data than expected while writing.
Number of times the NFS server indicated EJUKEBOX; retrieving data from offline storage.
Number of NFS v4.1+ pNFS reads.
Number of NFS v4.1+ pNFS writes.
A NFSOperationStats contains statistics for a single operation.
The name of the operation.
Number of requests performed for this operation.
Number of times an actual RPC request has been transmitted for this operation.
Number of times a request has had a major timeout.
Number of bytes sent for this operation, including RPC headers and payload.
Number of bytes received for this operation, including RPC headers and payload.
Duration all requests spent queued for transmission before they were sent.
Duration it took to get a reply back after the request was transmitted.
Duration from when a request was enqueued to when it was completely handled.
A NFSTransportStats contains statistics for the NFS mount RPC requests and responses.
The transport protocol used for the NFS mount.
The local port used for the NFS mount.
Number of times the client has had to establish a connection from scratch to the NFS server.
Number of times the client has made a TCP connection to the NFS server.
Duration (in jiffies, a kernel internal unit of time) the NFS mount has spent waiting for connections to the server to be established.
Duration since the NFS mount last saw any RPC traffic.
Number of RPC requests for this mount sent to the NFS server.
Number of RPC responses for this mount received from the NFS server.
Number of times the NFS server sent a response with a transaction ID unknown to this client.
A running counter, incremented on each request as the current difference ebetween sends and receives.
A running counter, incremented on each request by the current backlog queue size.
Stats below only available with stat version 1.1.
Maximum number of simultaneously active RPC requests ever used.
A running counter, incremented on each request as the current size of the sending queue.
A running counter, incremented on each request as the current size of the pending queue.
parseMountStats parses a /proc/[pid]/mountstats file and returns a slice of Mount structures containing detailed information about each mount. If available, statistics for each mount are parsed as well.
func ( io.Reader) ([]*Mount, error) {
	const (
		            = "device"
		 = "statvers="

		 = "nfs"
		 = "nfs4"
	)

	var  []*Mount

	 := bufio.NewScanner()
Only look for device entries in this function
		 := strings.Fields(string(.Bytes()))
		if len() == 0 || [0] !=  {
			continue
		}

		,  := parseMount()
		if  != nil {
			return nil, 
		}
Does this mount also possess statistics information?
Only NFSv3 and v4 are supported for parsing statistics
			if .Type !=  && .Type !=  {
				return nil, fmt.Errorf("cannot parse MountStats for fstype %q", .Type)
			}

			 := strings.TrimPrefix([8], )

			,  := parseMountStatsNFS(, )
			if  != nil {
				return nil, 
			}

			.Stats = 
		}

		 = append(, )
	}

	return , .Err()
}
parseMount parses an entry in /proc/[pid]/mountstats in the format: device [device] mounted on [mount] with fstype [type]
func ( []string) (*Mount, error) {
	if len() < deviceEntryLen {
		return nil, fmt.Errorf("invalid device entry: %v", )
	}
Check for specific words appearing at specific indices to ensure the format is consistent with what we expect
	 := []struct {
		 int
		 string
	}{
		{: 0, : "device"},
		{: 2, : "mounted"},
		{: 3, : "on"},
		{: 5, : "with"},
		{: 6, : "fstype"},
	}

	for ,  := range  {
		if [.] != . {
			return nil, fmt.Errorf("invalid device entry: %v", )
		}
	}

	return &Mount{
		Device: [1],
		Mount:  [4],
		Type:   [7],
	}, nil
}
parseMountStatsNFS parses a MountStatsNFS by scanning additional information related to NFS statistics.
Field indicators for parsing specific types of data
	const (
		       = "opts:"
		        = "age:"
		      = "bytes:"
		     = "events:"
		 = "per-op"
		  = "xprt:"
	)

	 := &MountStatsNFS{
		StatVersion: ,
	}

	for .Scan() {
		 := strings.Fields(string(.Bytes()))
		if len() == 0 {
			break
		}
		if len() < 2 {
			return nil, fmt.Errorf("not enough information for NFS stats: %v", )
		}

		switch [0] {
		case :
			if .Opts == nil {
				.Opts = map[string]string{}
			}
			for ,  := range strings.Split([1], ",") {
				 := strings.Split(, "=")
				if len() == 2 {
					.Opts[[0]] = [1]
				} else {
					.Opts[] = ""
				}
			}
Age integer is in seconds
			,  := time.ParseDuration([1] + "s")
			if  != nil {
				return nil, 
			}

			.Age = 
		case :
			,  := parseNFSBytesStats([1:])
			if  != nil {
				return nil, 
			}

			.Bytes = *
		case :
			,  := parseNFSEventsStats([1:])
			if  != nil {
				return nil, 
			}

			.Events = *
		case :
			if len() < 3 {
				return nil, fmt.Errorf("not enough information for NFS transport stats: %v", )
			}

			,  := parseNFSTransportStats([1:], )
			if  != nil {
				return nil, 
			}

			.Transport = *
		}
When encountering "per-operation statistics", we must break this loop and parse them separately to ensure we can terminate parsing before reaching another device entry; hence why this 'if' statement is not just another switch case
		if [0] ==  {
			break
		}
	}

	if  := .Err();  != nil {
		return nil, 
	}
NFS per-operation stats appear last before the next device entry
	,  := parseNFSOperationStats()
	if  != nil {
		return nil, 
	}

	.Operations = 

	return , nil
}
parseNFSBytesStats parses a NFSBytesStats line using an input set of integer fields.
func ( []string) (*NFSBytesStats, error) {
	if len() != fieldBytesLen {
		return nil, fmt.Errorf("invalid NFS bytes stats: %v", )
	}

	 := make([]uint64, 0, fieldBytesLen)
	for ,  := range  {
		,  := strconv.ParseUint(, 10, 64)
		if  != nil {
			return nil, 
		}

		 = append(, )
	}

	return &NFSBytesStats{
		Read:        [0],
		Write:       [1],
		DirectRead:  [2],
		DirectWrite: [3],
		ReadTotal:   [4],
		WriteTotal:  [5],
		ReadPages:   [6],
		WritePages:  [7],
	}, nil
}
parseNFSEventsStats parses a NFSEventsStats line using an input set of integer fields.
func ( []string) (*NFSEventsStats, error) {
	if len() != fieldEventsLen {
		return nil, fmt.Errorf("invalid NFS events stats: %v", )
	}

	 := make([]uint64, 0, fieldEventsLen)
	for ,  := range  {
		,  := strconv.ParseUint(, 10, 64)
		if  != nil {
			return nil, 
		}

		 = append(, )
	}

	return &NFSEventsStats{
		InodeRevalidate:     [0],
		DnodeRevalidate:     [1],
		DataInvalidate:      [2],
		AttributeInvalidate: [3],
		VFSOpen:             [4],
		VFSLookup:           [5],
		VFSAccess:           [6],
		VFSUpdatePage:       [7],
		VFSReadPage:         [8],
		VFSReadPages:        [9],
		VFSWritePage:        [10],
		VFSWritePages:       [11],
		VFSGetdents:         [12],
		VFSSetattr:          [13],
		VFSFlush:            [14],
		VFSFsync:            [15],
		VFSLock:             [16],
		VFSFileRelease:      [17],
		CongestionWait:      [18],
		Truncation:          [19],
		WriteExtension:      [20],
		SillyRename:         [21],
		ShortRead:           [22],
		ShortWrite:          [23],
		JukeboxDelay:        [24],
		PNFSRead:            [25],
		PNFSWrite:           [26],
	}, nil
}
parseNFSOperationStats parses a slice of NFSOperationStats by scanning additional information about per-operation statistics until an empty line is reached.
Number of expected fields in each per-operation statistics set
		 = 9
	)

	var  []NFSOperationStats

	for .Scan() {
		 := strings.Fields(string(.Bytes()))
Must break when reading a blank line after per-operation stats to enable top-level function to parse the next device entry
			break
		}

		if len() !=  {
			return nil, fmt.Errorf("invalid NFS per-operations stats: %v", )
		}
Skip string operation name for integers
		 := make([]uint64, 0, -1)
		for ,  := range [1:] {
			,  := strconv.ParseUint(, 10, 64)
			if  != nil {
				return nil, 
			}

			 = append(, )
		}

		 = append(, NFSOperationStats{
			Operation:                           strings.TrimSuffix([0], ":"),
			Requests:                            [0],
			Transmissions:                       [1],
			MajorTimeouts:                       [2],
			BytesSent:                           [3],
			BytesReceived:                       [4],
			CumulativeQueueMilliseconds:         [5],
			CumulativeTotalResponseMilliseconds: [6],
			CumulativeTotalRequestMilliseconds:  [7],
		})
	}

	return , .Err()
}
parseNFSTransportStats parses a NFSTransportStats line using an input set of integer fields matched to a specific stats version.
Extract the protocol field. It is the only string value in the line
	 := [0]
	 = [1:]

	switch  {
	case statVersion10:
		var  int
		if  == "tcp" {
			 = fieldTransport10TCPLen
		} else if  == "udp" {
			 = fieldTransport10UDPLen
		} else {
			return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.0 statement: %v", , )
		}
		if len() !=  {
			return nil, fmt.Errorf("invalid NFS transport stats 1.0 statement: %v", )
		}
	case statVersion11:
		var  int
		if  == "tcp" {
			 = fieldTransport11TCPLen
		} else if  == "udp" {
			 = fieldTransport11UDPLen
		} else {
			return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.1 statement: %v", , )
		}
		if len() !=  {
			return nil, fmt.Errorf("invalid NFS transport stats 1.1 statement: %v", )
		}
	default:
		return nil, fmt.Errorf("unrecognized NFS transport stats version: %q", )
	}
Allocate enough for v1.1 stats since zero value for v1.1 stats will be okay in a v1.0 response. Since the stat length is bigger for TCP stats, we use the TCP length here. Note: slice length must be set to length of v1.1 stats to avoid a panic when only v1.0 stats are present. See: https://github.com/prometheus/node_exporter/issues/571.
	 := make([]uint64, fieldTransport11TCPLen)
	for ,  := range  {
		,  := strconv.ParseUint(, 10, 64)
		if  != nil {
			return nil, 
		}

		[] = 
	}
The fields differ depending on the transport protocol (TCP or UDP) From https://utcc.utoronto.ca/%7Ecks/space/blog/linux/NFSMountstatsXprt For the udp RPC transport there is no connection count, connect idle time, or idle time (fields #3, #4, and #5); all other fields are the same. So we set them to 0 here.
	if  == "udp" {
		 = append([:2], append(make([]uint64, 3), [2:]...)...)
	}

	return &NFSTransportStats{
		Protocol:                 ,
		Port:                     [0],
		Bind:                     [1],
		Connect:                  [2],
		ConnectIdleTime:          [3],
		IdleTimeSeconds:          [4],
		Sends:                    [5],
		Receives:                 [6],
		BadTransactionIDs:        [7],
		CumulativeActiveRequests: [8],
		CumulativeBacklog:        [9],
		MaximumRPCSlotsUsed:      [10],
		CumulativeSendingQueue:   [11],
		CumulativePendingQueue:   [12],
	}, nil