494 lines
11 KiB
Go
494 lines
11 KiB
Go
package nfsclient
|
|
|
|
import (
|
|
"bufio"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
"github.com/influxdata/telegraf/internal/choice"
|
|
"github.com/influxdata/telegraf/plugins/inputs"
|
|
)
|
|
|
|
type NFSClient struct {
|
|
Fullstat bool `toml:"fullstat"`
|
|
IncludeMounts []string `toml:"include_mounts"`
|
|
ExcludeMounts []string `toml:"exclude_mounts"`
|
|
IncludeOperations []string `toml:"include_operations"`
|
|
ExcludeOperations []string `toml:"exclude_operations"`
|
|
Log telegraf.Logger `toml:"-"`
|
|
nfs3Ops map[string]bool
|
|
nfs4Ops map[string]bool
|
|
mountstatsPath string
|
|
}
|
|
|
|
const sampleConfig = `
|
|
## Read more low-level metrics (optional, defaults to false)
|
|
# fullstat = false
|
|
|
|
## List of mounts to explictly include or exclude (optional)
|
|
## The pattern (Go regexp) is matched against the mount point (not the
|
|
## device being mounted). If include_mounts is set, all mounts are ignored
|
|
## unless present in the list. If a mount is listed in both include_mounts
|
|
## and exclude_mounts, it is excluded. Go regexp patterns can be used.
|
|
# include_mounts = []
|
|
# exclude_mounts = []
|
|
|
|
## List of operations to include or exclude from collecting. This applies
|
|
## only when fullstat=true. Symantics are similar to {include,exclude}_mounts:
|
|
## the default is to collect everything; when include_operations is set, only
|
|
## those OPs are collected; when exclude_operations is set, all are collected
|
|
## except those listed. If include and exclude are set, the OP is excluded.
|
|
## See /proc/self/mountstats for a list of valid operations; note that
|
|
## NFSv3 and NFSv4 have different lists. While it is not possible to
|
|
## have different include/exclude lists for NFSv3/4, unused elements
|
|
## in the list should be okay. It is possible to have different lists
|
|
## for different mountpoints: use mulitple [[input.nfsclient]] stanzas,
|
|
## with their own lists. See "include_mounts" above, and be careful of
|
|
## duplicate metrics.
|
|
# include_operations = []
|
|
# exclude_operations = []
|
|
`
|
|
|
|
func (n *NFSClient) SampleConfig() string {
|
|
return sampleConfig
|
|
}
|
|
|
|
func (n *NFSClient) Description() string {
|
|
return "Read per-mount NFS client metrics from /proc/self/mountstats"
|
|
}
|
|
|
|
func convertToInt64(line []string) []int64 {
|
|
/* A "line" of input data (a pre-split array of strings) is
|
|
processed one field at a time. Each field is converted to
|
|
an int64 value, and appened to an array of return values.
|
|
On an error, check for ErrRange, and throw a fatal error
|
|
if found. This situation indicates a pretty major issue in
|
|
the /proc/self/mountstats file, and returning faulty data
|
|
is worse than no data. Other errors are ignored, and append
|
|
whatever we got in the first place (probably 0).
|
|
Yes, this is ugly. */
|
|
|
|
var nline []int64
|
|
|
|
if len(line) < 2 {
|
|
return nline
|
|
}
|
|
|
|
// Skip the first field; it's handled specially as the "first" variable
|
|
for _, l := range line[1:] {
|
|
val, err := strconv.ParseInt(l, 10, 64)
|
|
if err != nil {
|
|
if numError, ok := err.(*strconv.NumError); ok {
|
|
if numError.Err == strconv.ErrRange {
|
|
log.Fatalf("ErrRange: line:[%v] raw:[%v] -> parsed:[%v]\n", line, l, val)
|
|
}
|
|
}
|
|
}
|
|
nline = append(nline, val)
|
|
}
|
|
return nline
|
|
}
|
|
|
|
func (n *NFSClient) parseStat(mountpoint string, export string, version string, line []string, fullstat bool, acc telegraf.Accumulator) error {
|
|
tags := map[string]string{"mountpoint": mountpoint, "serverexport": export}
|
|
nline := convertToInt64(line)
|
|
|
|
if len(nline) == 0 {
|
|
n.Log.Warnf("Parsing Stat line with one field: %s\n", line)
|
|
return nil
|
|
}
|
|
|
|
first := strings.Replace(line[0], ":", "", 1)
|
|
|
|
var eventsFields = []string{
|
|
"inoderevalidates",
|
|
"dentryrevalidates",
|
|
"datainvalidates",
|
|
"attrinvalidates",
|
|
"vfsopen",
|
|
"vfslookup",
|
|
"vfsaccess",
|
|
"vfsupdatepage",
|
|
"vfsreadpage",
|
|
"vfsreadpages",
|
|
"vfswritepage",
|
|
"vfswritepages",
|
|
"vfsgetdents",
|
|
"vfssetattr",
|
|
"vfsflush",
|
|
"vfsfsync",
|
|
"vfslock",
|
|
"vfsrelease",
|
|
"congestionwait",
|
|
"setattrtrunc",
|
|
"extendwrite",
|
|
"sillyrenames",
|
|
"shortreads",
|
|
"shortwrites",
|
|
"delay",
|
|
"pnfsreads",
|
|
"pnfswrites",
|
|
}
|
|
|
|
var bytesFields = []string{
|
|
"normalreadbytes",
|
|
"normalwritebytes",
|
|
"directreadbytes",
|
|
"directwritebytes",
|
|
"serverreadbytes",
|
|
"serverwritebytes",
|
|
"readpages",
|
|
"writepages",
|
|
}
|
|
|
|
var xprtudpFields = []string{
|
|
"bind_count",
|
|
"rpcsends",
|
|
"rpcreceives",
|
|
"badxids",
|
|
"inflightsends",
|
|
"backlogutil",
|
|
}
|
|
|
|
var xprttcpFields = []string{
|
|
"bind_count",
|
|
"connect_count",
|
|
"connect_time",
|
|
"idle_time",
|
|
"rpcsends",
|
|
"rpcreceives",
|
|
"badxids",
|
|
"inflightsends",
|
|
"backlogutil",
|
|
}
|
|
|
|
var nfsopFields = []string{
|
|
"ops",
|
|
"trans",
|
|
"timeouts",
|
|
"bytes_sent",
|
|
"bytes_recv",
|
|
"queue_time",
|
|
"response_time",
|
|
"total_time",
|
|
"errors",
|
|
}
|
|
|
|
var fields = make(map[string]interface{})
|
|
|
|
switch first {
|
|
case "READ", "WRITE":
|
|
fields["ops"] = nline[0]
|
|
fields["retrans"] = nline[1] - nline[0]
|
|
fields["bytes"] = nline[3] + nline[4]
|
|
fields["rtt"] = nline[6]
|
|
fields["exe"] = nline[7]
|
|
tags["operation"] = first
|
|
acc.AddFields("nfsstat", fields, tags)
|
|
}
|
|
|
|
if fullstat {
|
|
switch first {
|
|
case "events":
|
|
if len(nline) >= len(eventsFields) {
|
|
for i, t := range eventsFields {
|
|
fields[t] = nline[i]
|
|
}
|
|
acc.AddFields("nfs_events", fields, tags)
|
|
}
|
|
|
|
case "bytes":
|
|
if len(nline) >= len(bytesFields) {
|
|
for i, t := range bytesFields {
|
|
fields[t] = nline[i]
|
|
}
|
|
acc.AddFields("nfs_bytes", fields, tags)
|
|
}
|
|
|
|
case "xprt":
|
|
if len(line) > 1 {
|
|
switch line[1] {
|
|
case "tcp":
|
|
if len(nline)+2 >= len(xprttcpFields) {
|
|
for i, t := range xprttcpFields {
|
|
fields[t] = nline[i+2]
|
|
}
|
|
acc.AddFields("nfs_xprt_tcp", fields, tags)
|
|
}
|
|
case "udp":
|
|
if len(nline)+2 >= len(xprtudpFields) {
|
|
for i, t := range xprtudpFields {
|
|
fields[t] = nline[i+2]
|
|
}
|
|
acc.AddFields("nfs_xprt_udp", fields, tags)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (version == "3" && n.nfs3Ops[first]) || (version == "4" && n.nfs4Ops[first]) {
|
|
tags["operation"] = first
|
|
if len(nline) <= len(nfsopFields) {
|
|
for i, t := range nline {
|
|
fields[nfsopFields[i]] = t
|
|
}
|
|
acc.AddFields("nfs_ops", fields, tags)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *NFSClient) processText(scanner *bufio.Scanner, acc telegraf.Accumulator) error {
|
|
var mount string
|
|
var version string
|
|
var export string
|
|
var skip bool
|
|
|
|
for scanner.Scan() {
|
|
line := strings.Fields(scanner.Text())
|
|
|
|
line_len := len(line)
|
|
|
|
if line_len == 0 {
|
|
continue
|
|
}
|
|
|
|
skip = false
|
|
|
|
// This denotes a new mount has been found, so set
|
|
// mount and export, and stop skipping (for now)
|
|
if line_len > 4 && choice.Contains("fstype", line) && (choice.Contains("nfs", line) || choice.Contains("nfs4", line)) {
|
|
mount = line[4]
|
|
export = line[1]
|
|
} else if line_len > 5 && (choice.Contains("(nfs)", line) || choice.Contains("(nfs4)", line)) {
|
|
version = strings.Split(line[5], "/")[1]
|
|
}
|
|
|
|
if mount == "" {
|
|
continue
|
|
}
|
|
|
|
if len(n.IncludeMounts) > 0 {
|
|
skip = true
|
|
for _, RE := range n.IncludeMounts {
|
|
matched, _ := regexp.MatchString(RE, mount)
|
|
if matched {
|
|
skip = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !skip && len(n.ExcludeMounts) > 0 {
|
|
for _, RE := range n.ExcludeMounts {
|
|
matched, _ := regexp.MatchString(RE, mount)
|
|
if matched {
|
|
skip = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !skip {
|
|
n.parseStat(mount, export, version, line, n.Fullstat, acc)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *NFSClient) getMountStatsPath() string {
|
|
path := "/proc/self/mountstats"
|
|
if os.Getenv("MOUNT_PROC") != "" {
|
|
path = os.Getenv("MOUNT_PROC")
|
|
}
|
|
n.Log.Debugf("using [%s] for mountstats", path)
|
|
return path
|
|
}
|
|
|
|
func (n *NFSClient) Gather(acc telegraf.Accumulator) error {
|
|
file, err := os.Open(n.mountstatsPath)
|
|
if err != nil {
|
|
n.Log.Errorf("Failed opening the [%s] file: %s ", file, err)
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
n.processText(scanner, acc)
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
n.Log.Errorf("%s", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *NFSClient) Init() error {
|
|
var nfs3Fields = []string{
|
|
"NULL",
|
|
"GETATTR",
|
|
"SETATTR",
|
|
"LOOKUP",
|
|
"ACCESS",
|
|
"READLINK",
|
|
"READ",
|
|
"WRITE",
|
|
"CREATE",
|
|
"MKDIR",
|
|
"SYMLINK",
|
|
"MKNOD",
|
|
"REMOVE",
|
|
"RMDIR",
|
|
"RENAME",
|
|
"LINK",
|
|
"READDIR",
|
|
"READDIRPLUS",
|
|
"FSSTAT",
|
|
"FSINFO",
|
|
"PATHCONF",
|
|
"COMMIT",
|
|
}
|
|
|
|
var nfs4Fields = []string{
|
|
"NULL",
|
|
"READ",
|
|
"WRITE",
|
|
"COMMIT",
|
|
"OPEN",
|
|
"OPEN_CONFIRM",
|
|
"OPEN_NOATTR",
|
|
"OPEN_DOWNGRADE",
|
|
"CLOSE",
|
|
"SETATTR",
|
|
"FSINFO",
|
|
"RENEW",
|
|
"SETCLIENTID",
|
|
"SETCLIENTID_CONFIRM",
|
|
"LOCK",
|
|
"LOCKT",
|
|
"LOCKU",
|
|
"ACCESS",
|
|
"GETATTR",
|
|
"LOOKUP",
|
|
"LOOKUP_ROOT",
|
|
"REMOVE",
|
|
"RENAME",
|
|
"LINK",
|
|
"SYMLINK",
|
|
"CREATE",
|
|
"PATHCONF",
|
|
"STATFS",
|
|
"READLINK",
|
|
"READDIR",
|
|
"SERVER_CAPS",
|
|
"DELEGRETURN",
|
|
"GETACL",
|
|
"SETACL",
|
|
"FS_LOCATIONS",
|
|
"RELEASE_LOCKOWNER",
|
|
"SECINFO",
|
|
"FSID_PRESENT",
|
|
"EXCHANGE_ID",
|
|
"CREATE_SESSION",
|
|
"DESTROY_SESSION",
|
|
"SEQUENCE",
|
|
"GET_LEASE_TIME",
|
|
"RECLAIM_COMPLETE",
|
|
"LAYOUTGET",
|
|
"GETDEVICEINFO",
|
|
"LAYOUTCOMMIT",
|
|
"LAYOUTRETURN",
|
|
"SECINFO_NO_NAME",
|
|
"TEST_STATEID",
|
|
"FREE_STATEID",
|
|
"GETDEVICELIST",
|
|
"BIND_CONN_TO_SESSION",
|
|
"DESTROY_CLIENTID",
|
|
"SEEK",
|
|
"ALLOCATE",
|
|
"DEALLOCATE",
|
|
"LAYOUTSTATS",
|
|
"CLONE",
|
|
"COPY",
|
|
"OFFLOAD_CANCEL",
|
|
"LOOKUPP",
|
|
"LAYOUTERROR",
|
|
"COPY_NOTIFY",
|
|
"GETXATTR",
|
|
"SETXATTR",
|
|
"LISTXATTRS",
|
|
"REMOVEXATTR",
|
|
}
|
|
|
|
nfs3Ops := make(map[string]bool)
|
|
nfs4Ops := make(map[string]bool)
|
|
|
|
n.mountstatsPath = n.getMountStatsPath()
|
|
|
|
if len(n.IncludeOperations) == 0 {
|
|
for _, Op := range nfs3Fields {
|
|
nfs3Ops[Op] = true
|
|
}
|
|
for _, Op := range nfs4Fields {
|
|
nfs4Ops[Op] = true
|
|
}
|
|
} else {
|
|
for _, Op := range n.IncludeOperations {
|
|
nfs3Ops[Op] = true
|
|
}
|
|
for _, Op := range n.IncludeOperations {
|
|
nfs4Ops[Op] = true
|
|
}
|
|
}
|
|
|
|
if len(n.ExcludeOperations) > 0 {
|
|
for _, Op := range n.ExcludeOperations {
|
|
if nfs3Ops[Op] {
|
|
delete(nfs3Ops, Op)
|
|
}
|
|
if nfs4Ops[Op] {
|
|
delete(nfs4Ops, Op)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(n.IncludeMounts) > 0 {
|
|
n.Log.Debugf("Including these mount patterns: %v", n.IncludeMounts)
|
|
} else {
|
|
n.Log.Debugf("Including all mounts.")
|
|
}
|
|
|
|
if len(n.ExcludeMounts) > 0 {
|
|
n.Log.Debugf("Excluding these mount patterns: %v", n.ExcludeMounts)
|
|
} else {
|
|
n.Log.Debugf("Not excluding any mounts.")
|
|
}
|
|
|
|
if len(n.IncludeOperations) > 0 {
|
|
n.Log.Debugf("Including these operations: %v", n.IncludeOperations)
|
|
} else {
|
|
n.Log.Debugf("Including all operations.")
|
|
}
|
|
|
|
if len(n.ExcludeOperations) > 0 {
|
|
n.Log.Debugf("Excluding these mount patterns: %v", n.ExcludeOperations)
|
|
} else {
|
|
n.Log.Debugf("Not excluding any operations.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
inputs.Add("nfsclient", func() telegraf.Input {
|
|
return &NFSClient{}
|
|
})
|
|
}
|