diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 93ea3e779..49e7918aa 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -35,6 +35,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/net_response" _ "github.com/influxdata/telegraf/plugins/inputs/nginx" _ "github.com/influxdata/telegraf/plugins/inputs/nsq" + _ "github.com/influxdata/telegraf/plugins/inputs/nstat" _ "github.com/influxdata/telegraf/plugins/inputs/ntpq" _ "github.com/influxdata/telegraf/plugins/inputs/passenger" _ "github.com/influxdata/telegraf/plugins/inputs/phpfpm" diff --git a/plugins/inputs/nstat/README.md b/plugins/inputs/nstat/README.md new file mode 100644 index 000000000..1b786fdcd --- /dev/null +++ b/plugins/inputs/nstat/README.md @@ -0,0 +1,3 @@ +## Nstat input plugin + +Plugin collects network metrics from ```/proc/net/netstat```, ```/proc/net/snmp``` and ```/proc/net/snmp6``` files diff --git a/plugins/inputs/nstat/nstat.go b/plugins/inputs/nstat/nstat.go new file mode 100644 index 000000000..2603b1ca0 --- /dev/null +++ b/plugins/inputs/nstat/nstat.go @@ -0,0 +1,183 @@ +package nstat + +import ( + "bytes" + "io/ioutil" + "strconv" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" +) + +var ( + zeroByte = []byte("0") + newLineByte = []byte("\n") + colonByte = []byte(":") +) + +type Nstat struct { + ProcNetNetstat string `toml:"proc_net_netstat"` + ProcNetSNMP string `toml:"proc_net_snmp"` + ProcNetSNMP6 string `toml:"proc_net_snmp6"` + DumpZeros bool `toml:"dump_zeros"` +} + +var sampleConfig = ` + # file paths + proc_net_netstat = "/proc/net/netstat" + proc_net_snmp = "/proc/net/snmp" + proc_net_snmp6 = "/proc/net/snmp6" + # dump metrics with 0 values too + dump_zeros = true +` + +func (ns *Nstat) Description() string { + return "Collect network metrics from '/proc/net/netstat', '/proc/net/snmp' & '/proc/net/snmp6' files" +} + +func (ns *Nstat) SampleConfig() string { + return sampleConfig +} + +func (ns *Nstat) Gather(acc telegraf.Accumulator) error { + netstat, err := ioutil.ReadFile(ns.ProcNetNetstat) + if err != nil { + return err + } + + // collect netstat data + err = ns.gatherNetstat(netstat, acc) + if err != nil { + return err + } + + // collect SNMP data + snmp, err := ioutil.ReadFile(ns.ProcNetSNMP) + if err != nil { + return err + } + err = ns.gatherSNMP(snmp, acc) + if err != nil { + return err + } + + // collect SNMP6 data + snmp6, err := ioutil.ReadFile(ns.ProcNetSNMP6) + if err != nil { + return err + } + err = ns.gatherSNMP6(snmp6, acc) + if err != nil { + return err + } + return nil +} + +func (ns *Nstat) gatherNetstat(data []byte, acc telegraf.Accumulator) error { + metrics, err := loadUglyTable(data, ns.DumpZeros) + if err != nil { + return err + } + tags := map[string]string{ + "name": "netstat", + } + acc.AddFields("nstat", metrics, tags) + return nil +} + +func (ns *Nstat) gatherSNMP(data []byte, acc telegraf.Accumulator) error { + metrics, err := loadUglyTable(data, ns.DumpZeros) + if err != nil { + return err + } + tags := map[string]string{ + "name": "snmp", + } + acc.AddFields("nstat", metrics, tags) + return nil +} + +func (ns *Nstat) gatherSNMP6(data []byte, acc telegraf.Accumulator) error { + metrics, err := loadGoodTable(data, ns.DumpZeros) + if err != nil { + return err + } + tags := map[string]string{ + "name": "snmp6", + } + acc.AddFields("nstat", metrics, tags) + return nil +} + +// loadGoodTable can be used to parse string heap that +// headers and values are arranged in right order +func loadGoodTable(table []byte, dumpZeros bool) (map[string]interface{}, error) { + entries := map[string]interface{}{} + fields := bytes.Fields(table) + var value int64 + var err error + // iterate over two values each time + // first value is header, second is value + for i := 0; i < len(fields); i = i + 2 { + // counter is zero + if bytes.Equal(fields[i+1], zeroByte) { + if !dumpZeros { + continue + } else { + entries[string(fields[i])] = int64(0) + continue + } + } + // the counter is not zero, so parse it. + value, err = strconv.ParseInt(string(fields[i+1]), 10, 64) + if err == nil { + entries[string(fields[i])] = value + } + } + return entries, nil +} + +// loadUglyTable can be used to parse string heap that +// the headers and values are splitted with a newline +func loadUglyTable(table []byte, dumpZeros bool) (map[string]interface{}, error) { + entries := map[string]interface{}{} + // split the lines by newline + lines := bytes.Split(table, newLineByte) + var value int64 + var err error + // iterate over lines, take 2 lines each time + // first line contains header names + // second line contains values + for i := 0; i < len(lines); i = i + 2 { + if len(lines[i]) == 0 { + continue + } + headers := bytes.Fields(lines[i]) + prefix := bytes.TrimSuffix(headers[0], colonByte) + metrics := bytes.Fields(lines[i+1]) + + for j := 1; j < len(headers); j++ { + // counter is zero + if bytes.Equal(metrics[j], zeroByte) { + if !dumpZeros { + continue + } else { + entries[string(append(prefix, headers[j]...))] = int64(0) + continue + } + } + // the counter is not zero, so parse it. + value, err = strconv.ParseInt(string(metrics[j]), 10, 64) + if err == nil { + entries[string(append(prefix, headers[j]...))] = value + } + } + } + return entries, nil +} + +func init() { + inputs.Add("nstat", func() telegraf.Input { + return &Nstat{} + }) +} diff --git a/plugins/inputs/nstat/nstat_test.go b/plugins/inputs/nstat/nstat_test.go new file mode 100644 index 000000000..7f4c09ce4 --- /dev/null +++ b/plugins/inputs/nstat/nstat_test.go @@ -0,0 +1,56 @@ +package nstat + +import "testing" + +func TestLoadUglyTable(t *testing.T) { + uglyStr := `IpExt: InNoRoutes InTruncatedPkts InMcastPkts InCEPkts + IpExt: 332 433718 0 2660494435` + parsed := map[string]interface{}{ + "IpExtInNoRoutes": int64(332), + "IpExtInTruncatedPkts": int64(433718), + "IpExtInMcastPkts": int64(0), + "IpExtInCEPkts": int64(2660494435), + } + + got, err := loadUglyTable([]byte(uglyStr), true) + if err != nil { + t.Fatal(err) + } + + if len(got) == 0 { + t.Fatalf("want %+v, got %+v", parsed, got) + } + + for key := range parsed { + if parsed[key].(int64) != got[key].(int64) { + t.Fatalf("want %+v, got %+v", parsed[key], got[key]) + } + } +} + +func TestLoadGoodTable(t *testing.T) { + goodStr := `Ip6InReceives 11707 + Ip6InTooBigErrors 0 + Ip6InDelivers 62 + Ip6InMcastOctets 1242966` + + parsed := map[string]interface{}{ + "Ip6InReceives": int64(11707), + "Ip6InTooBigErrors": int64(0), + "Ip6InDelivers": int64(62), + "Ip6InMcastOctets": int64(1242966), + } + got, err := loadGoodTable([]byte(goodStr), true) + if err != nil { + t.Fatal(err) + } + if len(got) == 0 { + t.Fatalf("want %+v, got %+v", parsed, got) + } + + for key := range parsed { + if parsed[key].(int64) != got[key].(int64) { + t.Fatalf("want %+v, got %+v", parsed[key], got[key]) + } + } +}