From 28314f93b6d12d3a8e8af96312b1b440475f3f85 Mon Sep 17 00:00:00 2001 From: Patrick Hemmer Date: Mon, 4 Jul 2016 18:48:37 -0400 Subject: [PATCH] add support for diskio name templates & udev tags closes #1453 closes #1386 closes #1428 --- CHANGELOG.md | 1 + plugins/inputs/system/disk.go | 85 ++++++++++++++++++- plugins/inputs/system/disk_linux.go | 66 +++++++++++++++ plugins/inputs/system/disk_linux_test.go | 101 +++++++++++++++++++++++ plugins/inputs/system/disk_other.go | 9 ++ 5 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 plugins/inputs/system/disk_linux.go create mode 100644 plugins/inputs/system/disk_linux_test.go create mode 100644 plugins/inputs/system/disk_other.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d7f635e..777eefbdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ It is highly recommended that all users migrate to the new riemann output plugin - [#2179](https://github.com/influxdata/telegraf/pull/2179): Added more InnoDB metric to MySQL plugin. - [#2251](https://github.com/influxdata/telegraf/pull/2251): InfluxDB output: use own client for improved through-put and less allocations. - [#1900](https://github.com/influxdata/telegraf/pull/1900): Riemann plugin rewrite. +- [#1453](https://github.com/influxdata/telegraf/pull/1453): diskio: add support for name templates and udev tags. ### Bugfixes diff --git a/plugins/inputs/system/disk.go b/plugins/inputs/system/disk.go index 548a9ce23..3f6d83c1c 100644 --- a/plugins/inputs/system/disk.go +++ b/plugins/inputs/system/disk.go @@ -2,6 +2,7 @@ package system import ( "fmt" + "regexp" "strings" "github.com/influxdata/telegraf" @@ -82,7 +83,11 @@ type DiskIOStats struct { ps PS Devices []string + DeviceTags []string + NameTemplates []string SkipSerialNumber bool + + infoCache map[string]diskInfoCache } func (_ *DiskIOStats) Description() string { @@ -96,6 +101,23 @@ var diskIoSampleConfig = ` # devices = ["sda", "sdb"] ## Uncomment the following line if you need disk serial numbers. # skip_serial_number = false + # + ## On systems which support it, device metadata can be added in the form of + ## tags. + ## Currently only Linux is supported via udev properties. You can view + ## available properties for a device by running: + ## 'udevadm info -q property -n /dev/sda' + # device_tags = ["ID_FS_TYPE", "ID_FS_USAGE"] + # + ## Using the same metadata source as device_tags, you can also customize the + ## name of the device via templates. + ## The 'name_templates' parameter is a list of templates to try and apply to + ## the device. The template may contain variables in the form of '$PROPERTY' or + ## '${PROPERTY}'. The first template which does not contain any variables not + ## present for the device is used as the device name tag. + ## The typical use case is for LVM volumes, to get the VG/LV name instead of + ## the near-meaningless DM-0 name. + # name_templates = ["$ID_FS_LABEL","$DM_VG_NAME/$DM_LV_NAME"] ` func (_ *DiskIOStats) SampleConfig() string { @@ -123,7 +145,10 @@ func (s *DiskIOStats) Gather(acc telegraf.Accumulator) error { continue } tags := map[string]string{} - tags["name"] = io.Name + tags["name"] = s.diskName(io.Name) + for t, v := range s.diskTags(io.Name) { + tags[t] = v + } if !s.SkipSerialNumber { if len(io.SerialNumber) != 0 { tags["serial"] = io.SerialNumber @@ -148,6 +173,64 @@ func (s *DiskIOStats) Gather(acc telegraf.Accumulator) error { return nil } +var varRegex = regexp.MustCompile(`\$(?:\w+|\{\w+\})`) + +func (s *DiskIOStats) diskName(devName string) string { + di, err := s.diskInfo(devName) + if err != nil { + // discard error :-( + // We can't return error because it's non-fatal to the Gather(). + // And we have no logger, so we can't log it. + return devName + } + if di == nil { + return devName + } + + for _, nt := range s.NameTemplates { + miss := false + name := varRegex.ReplaceAllStringFunc(nt, func(sub string) string { + sub = sub[1:] // strip leading '$' + if sub[0] == '{' { + sub = sub[1 : len(sub)-1] // strip leading & trailing '{' '}' + } + if v, ok := di[sub]; ok { + return v + } + miss = true + return "" + }) + + if !miss { + return name + } + } + + return devName +} + +func (s *DiskIOStats) diskTags(devName string) map[string]string { + di, err := s.diskInfo(devName) + if err != nil { + // discard error :-( + // We can't return error because it's non-fatal to the Gather(). + // And we have no logger, so we can't log it. + return nil + } + if di == nil { + return nil + } + + tags := map[string]string{} + for _, dt := range s.DeviceTags { + if v, ok := di[dt]; ok { + tags[dt] = v + } + } + + return tags +} + func init() { inputs.Add("disk", func() telegraf.Input { return &DiskStats{ps: &systemPS{}} diff --git a/plugins/inputs/system/disk_linux.go b/plugins/inputs/system/disk_linux.go new file mode 100644 index 000000000..e5a0cff55 --- /dev/null +++ b/plugins/inputs/system/disk_linux.go @@ -0,0 +1,66 @@ +package system + +import ( + "bufio" + "fmt" + "os" + "strings" + "syscall" +) + +type diskInfoCache struct { + stat syscall.Stat_t + values map[string]string +} + +var udevPath = "/run/udev/data" + +func (s *DiskIOStats) diskInfo(devName string) (map[string]string, error) { + fi, err := os.Stat("/dev/" + devName) + if err != nil { + return nil, err + } + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return nil, nil + } + + if s.infoCache == nil { + s.infoCache = map[string]diskInfoCache{} + } + ic, ok := s.infoCache[devName] + if ok { + return ic.values, nil + } else { + ic = diskInfoCache{ + stat: *stat, + values: map[string]string{}, + } + s.infoCache[devName] = ic + } + di := ic.values + + major := stat.Rdev >> 8 & 0xff + minor := stat.Rdev & 0xff + + f, err := os.Open(fmt.Sprintf("%s/b%d:%d", udevPath, major, minor)) + if err != nil { + return nil, err + } + defer f.Close() + scnr := bufio.NewScanner(f) + + for scnr.Scan() { + l := scnr.Text() + if len(l) < 4 || l[:2] != "E:" { + continue + } + kv := strings.SplitN(l[2:], "=", 2) + if len(kv) < 2 { + continue + } + di[kv[0]] = kv[1] + } + + return di, nil +} diff --git a/plugins/inputs/system/disk_linux_test.go b/plugins/inputs/system/disk_linux_test.go new file mode 100644 index 000000000..801ad328a --- /dev/null +++ b/plugins/inputs/system/disk_linux_test.go @@ -0,0 +1,101 @@ +// +build linux + +package system + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var nullDiskInfo = []byte(` +E:MY_PARAM_1=myval1 +E:MY_PARAM_2=myval2 +`) + +// setupNullDisk sets up fake udev info as if /dev/null were a disk. +func setupNullDisk(t *testing.T) func() error { + td, err := ioutil.TempDir("", ".telegraf.TestDiskInfo") + require.NoError(t, err) + + origUdevPath := udevPath + + cleanFunc := func() error { + udevPath = origUdevPath + return os.RemoveAll(td) + } + + udevPath = td + err = ioutil.WriteFile(td+"/b1:3", nullDiskInfo, 0644) // 1:3 is the 'null' device + if err != nil { + cleanFunc() + t.Fatal(err) + } + + return cleanFunc +} + +func TestDiskInfo(t *testing.T) { + clean := setupNullDisk(t) + defer clean() + + s := &DiskIOStats{} + di, err := s.diskInfo("null") + require.NoError(t, err) + assert.Equal(t, "myval1", di["MY_PARAM_1"]) + assert.Equal(t, "myval2", di["MY_PARAM_2"]) + + // test that data is cached + err = clean() + require.NoError(t, err) + + di, err = s.diskInfo("null") + require.NoError(t, err) + assert.Equal(t, "myval1", di["MY_PARAM_1"]) + assert.Equal(t, "myval2", di["MY_PARAM_2"]) + + // unfortunately we can't adjust mtime on /dev/null to test cache invalidation +} + +// DiskIOStats.diskName isn't a linux specific function, but dependent +// functions are a no-op on non-Linux. +func TestDiskIOStats_diskName(t *testing.T) { + defer setupNullDisk(t)() + + tests := []struct { + templates []string + expected string + }{ + {[]string{"$MY_PARAM_1"}, "myval1"}, + {[]string{"${MY_PARAM_1}"}, "myval1"}, + {[]string{"x$MY_PARAM_1"}, "xmyval1"}, + {[]string{"x${MY_PARAM_1}x"}, "xmyval1x"}, + {[]string{"$MISSING", "$MY_PARAM_1"}, "myval1"}, + {[]string{"$MY_PARAM_1", "$MY_PARAM_2"}, "myval1"}, + {[]string{"$MISSING"}, "null"}, + {[]string{"$MY_PARAM_1/$MY_PARAM_2"}, "myval1/myval2"}, + {[]string{"$MY_PARAM_2/$MISSING"}, "null"}, + } + + for _, tc := range tests { + s := DiskIOStats{ + NameTemplates: tc.templates, + } + assert.Equal(t, tc.expected, s.diskName("null"), "Templates: %#v", tc.templates) + } +} + +// DiskIOStats.diskTags isn't a linux specific function, but dependent +// functions are a no-op on non-Linux. +func TestDiskIOStats_diskTags(t *testing.T) { + defer setupNullDisk(t)() + + s := &DiskIOStats{ + DeviceTags: []string{"MY_PARAM_2"}, + } + dt := s.diskTags("null") + assert.Equal(t, map[string]string{"MY_PARAM_2": "myval2"}, dt) +} diff --git a/plugins/inputs/system/disk_other.go b/plugins/inputs/system/disk_other.go new file mode 100644 index 000000000..fa9121cdf --- /dev/null +++ b/plugins/inputs/system/disk_other.go @@ -0,0 +1,9 @@ +// +build !linux + +package system + +type diskInfoCache struct{} + +func (s *DiskIOStats) diskInfo(devName string) (map[string]string, error) { + return nil, nil +}