fix(inputs.diskio): Add missing udev properties (#15003)

This commit is contained in:
Sven Rebhan 2024-03-18 22:05:00 +01:00 committed by GitHub
parent 13c786bdfa
commit c9fb4e74be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 171 additions and 158 deletions

View File

@ -16,24 +16,20 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
```toml @sample.conf ```toml @sample.conf
# Read metrics about disk IO by device # Read metrics about disk IO by device
[[inputs.diskio]] [[inputs.diskio]]
## By default, telegraf will gather stats for all devices including ## Devices to collect stats for
## disk partitions. ## Wildcards are supported except for disk synonyms like '/dev/disk/by-id'.
## Setting devices will restrict the stats to the specified devices. ## ex. devices = ["sda", "sdb", "vd*", "/dev/disk/by-id/nvme-eui.00123deadc0de123"]
## NOTE: Globbing expressions (e.g. asterix) are not supported for # devices = ["*"]
## disk synonyms like '/dev/disk/by-id'.
# devices = ["sda", "sdb", "vd*", "/dev/disk/by-id/nvme-eui.00123deadc0de123"] ## Skip gathering of the disk's serial numbers.
## Uncomment the following line if you need disk serial numbers. # skip_serial_number = true
# skip_serial_number = false
# ## Device metadata tags to add on systems supporting it (Linux only)
## On systems which support it, device metadata can be added in the form of ## Use 'udevadm info -q property -n <device>' to get a list of properties.
## 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'
## Note: Most, but not all, udev properties can be accessed this way. Properties ## Note: Most, but not all, udev properties can be accessed this way. Properties
## that are currently inaccessible include DEVTYPE, DEVNAME, and DEVPATH. ## that are currently inaccessible include DEVTYPE, DEVNAME, and DEVPATH.
# device_tags = ["ID_FS_TYPE", "ID_FS_USAGE"] # device_tags = ["ID_FS_TYPE", "ID_FS_USAGE"]
#
## Using the same metadata source as device_tags, you can also customize the ## Using the same metadata source as device_tags, you can also customize the
## name of the device via templates. ## name of the device via templates.
## The 'name_templates' parameter is a list of templates to try and apply to ## The 'name_templates' parameter is a list of templates to try and apply to

View File

@ -25,6 +25,18 @@ func hasMeta(s string) bool {
return strings.ContainsAny(s, "*?[") return strings.ContainsAny(s, "*?[")
} }
type DiskIO struct {
Devices []string `toml:"devices"`
DeviceTags []string `toml:"device_tags"`
NameTemplates []string `toml:"name_templates"`
SkipSerialNumber bool `toml:"skip_serial_number"`
Log telegraf.Logger `toml:"-"`
ps system.PS
infoCache map[string]diskInfoCache
deviceFilter filter.Filter
}
func (*DiskIO) SampleConfig() string { func (*DiskIO) SampleConfig() string {
return sampleConfig return sampleConfig
} }
@ -39,6 +51,9 @@ func (d *DiskIO) Init() error {
d.deviceFilter = deviceFilter d.deviceFilter = deviceFilter
} }
} }
d.infoCache = make(map[string]diskInfoCache)
return nil return nil
} }

View File

@ -11,51 +11,30 @@ import (
"strings" "strings"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/plugins/inputs/system"
) )
type DiskIO struct {
ps system.PS
Devices []string
DeviceTags []string
NameTemplates []string
SkipSerialNumber bool
Log telegraf.Logger
infoCache map[string]diskInfoCache
deviceFilter filter.Filter
}
type diskInfoCache struct { type diskInfoCache struct {
modifiedAt int64 // Unix Nano timestamp of the last modification of the device. This value is used to invalidate the cache modifiedAt int64 // Unix Nano timestamp of the last modification of the device. This value is used to invalidate the cache
udevDataPath string udevDataPath string
sysBlockPath string
values map[string]string values map[string]string
} }
func (d *DiskIO) diskInfo(devName string) (map[string]string, error) { func (d *DiskIO) diskInfo(devName string) (map[string]string, error) {
var err error // Check if the device exists
var stat unix.Stat_t
path := "/dev/" + devName path := "/dev/" + devName
err = unix.Stat(path, &stat) var stat unix.Stat_t
if err != nil { if err := unix.Stat(path, &stat); err != nil {
return nil, err return nil, err
} }
if d.infoCache == nil { // Check if we already got a cached and valid entry
d.infoCache = map[string]diskInfoCache{}
}
ic, ok := d.infoCache[devName] ic, ok := d.infoCache[devName]
if ok && stat.Mtim.Nano() == ic.modifiedAt { if ok && stat.Mtim.Nano() == ic.modifiedAt {
return ic.values, nil return ic.values, nil
} }
// Determine udev properties
var udevDataPath string var udevDataPath string
if ok && len(ic.udevDataPath) > 0 { if ok && len(ic.udevDataPath) > 0 {
// We can reuse the udev data path from a "previous" entry. // We can reuse the udev data path from a "previous" entry.
@ -65,33 +44,60 @@ func (d *DiskIO) diskInfo(devName string) (map[string]string, error) {
major := unix.Major(uint64(stat.Rdev)) //nolint:unconvert // Conversion needed for some architectures major := unix.Major(uint64(stat.Rdev)) //nolint:unconvert // Conversion needed for some architectures
minor := unix.Minor(uint64(stat.Rdev)) //nolint:unconvert // Conversion needed for some architectures minor := unix.Minor(uint64(stat.Rdev)) //nolint:unconvert // Conversion needed for some architectures
udevDataPath = fmt.Sprintf("/run/udev/data/b%d:%d", major, minor) udevDataPath = fmt.Sprintf("/run/udev/data/b%d:%d", major, minor)
if _, err := os.Stat(udevDataPath); err != nil {
_, err := os.Stat(udevDataPath)
if err != nil {
// This path failed, try the fallback .udev style (non-systemd) // This path failed, try the fallback .udev style (non-systemd)
udevDataPath = "/dev/.udev/db/block:" + devName udevDataPath = "/dev/.udev/db/block:" + devName
_, err := os.Stat(udevDataPath) if _, err := os.Stat(udevDataPath); err != nil {
if err != nil {
// Giving up, cannot retrieve disk info // Giving up, cannot retrieve disk info
return nil, err return nil, err
} }
} }
} }
info, err := readUdevData(udevDataPath)
if err != nil {
return nil, err
}
// Read additional device properties
var sysBlockPath string
if ok && len(ic.sysBlockPath) > 0 {
// We can reuse the /sys block path from a "previous" entry.
// This allows us to also "poison" it during test scenarios
sysBlockPath = ic.sysBlockPath
} else {
sysBlockPath = "/sys/block/" + devName
if _, err := os.Stat(sysBlockPath); err != nil {
// Giving up, cannot retrieve additional info
return nil, err
}
}
devInfo, err := readDevData(sysBlockPath)
if err != nil {
return nil, err
}
for k, v := range devInfo {
info[k] = v
}
d.infoCache[devName] = diskInfoCache{
modifiedAt: stat.Mtim.Nano(),
udevDataPath: udevDataPath,
values: info,
}
return info, nil
}
func readUdevData(path string) (map[string]string, error) {
// Final open of the confirmed (or the previously detected/used) udev file // Final open of the confirmed (or the previously detected/used) udev file
f, err := os.Open(udevDataPath) f, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer f.Close()
di := map[string]string{} info := make(map[string]string)
d.infoCache[devName] = diskInfoCache{
modifiedAt: stat.Mtim.Nano(),
udevDataPath: udevDataPath,
values: di,
}
scnr := bufio.NewScanner(f) scnr := bufio.NewScanner(f)
var devlinks bytes.Buffer var devlinks bytes.Buffer
for scnr.Scan() { for scnr.Scan() {
@ -114,14 +120,51 @@ func (d *DiskIO) diskInfo(devName string) (map[string]string, error) {
if len(kv) < 2 { if len(kv) < 2 {
continue continue
} }
di[kv[0]] = kv[1] info[kv[0]] = kv[1]
} }
if devlinks.Len() > 0 { if devlinks.Len() > 0 {
di["DEVLINKS"] = devlinks.String() info["DEVLINKS"] = devlinks.String()
} }
return di, nil return info, nil
}
func readDevData(path string) (map[string]string, error) {
// Open the file and read line-wise
f, err := os.Open(filepath.Join(path, "uevent"))
if err != nil {
return nil, err
}
defer f.Close()
// Read DEVNAME and DEVTYPE
info := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "DEV") {
continue
}
k, v, found := strings.Cut(line, "=")
if !found {
continue
}
info[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
if d, found := info["DEVNAME"]; found && !strings.HasPrefix(d, "/dev") {
info["DEVNAME"] = "/dev/" + d
}
// Find the DEVPATH property
if devlnk, err := filepath.EvalSymlinks(filepath.Join(path, "device")); err == nil {
devlnk = filepath.Join(devlnk, filepath.Base(path))
devlnk = strings.TrimPrefix(devlnk, "/sys")
info["DEVPATH"] = devlnk
}
return info, nil
} }
func resolveName(name string) string { func resolveName(name string) string {
@ -129,7 +172,7 @@ func resolveName(name string) string {
if err == nil { if err == nil {
return resolved return resolved
} }
if err != nil && !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return name return name
} }
// Try to prepend "/dev" // Try to prepend "/dev"

View File

@ -3,72 +3,29 @@
package diskio package diskio
import ( import (
"os" "fmt"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var nullDiskInfo = []byte(`
E:MY_PARAM_1=myval1
E:MY_PARAM_2=myval2
S:foo/bar/devlink
S:foo/bar/devlink1
`)
// setupNullDisk sets up fake udev info as if /dev/null were a disk.
func setupNullDisk(t *testing.T, s *DiskIO, devName string) func() {
td, err := os.CreateTemp("", ".telegraf.DiskInfoTest")
require.NoError(t, err)
if s.infoCache == nil {
s.infoCache = make(map[string]diskInfoCache)
}
ic, ok := s.infoCache[devName]
if !ok {
// No previous calls for the device were done, easy to poison the cache
s.infoCache[devName] = diskInfoCache{
modifiedAt: 0,
udevDataPath: td.Name(),
values: map[string]string{},
}
}
origUdevPath := ic.udevDataPath
cleanFunc := func() {
ic.udevDataPath = origUdevPath
os.Remove(td.Name())
}
ic.udevDataPath = td.Name()
_, err = td.Write(nullDiskInfo)
if err != nil {
cleanFunc()
t.Fatal(err)
}
return cleanFunc
}
func TestDiskInfo(t *testing.T) { func TestDiskInfo(t *testing.T) {
s := &DiskIO{} plugin := &DiskIO{
clean := setupNullDisk(t, s, "null") infoCache: map[string]diskInfoCache{
defer clean() "null": {
di, err := s.diskInfo("null") modifiedAt: 0,
udevDataPath: "testdata/udev.txt",
sysBlockPath: "testdata",
values: map[string]string{},
},
},
}
di, err := plugin.diskInfo("null")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "myval1", di["MY_PARAM_1"]) require.Equal(t, "myval1", di["MY_PARAM_1"])
require.Equal(t, "myval2", di["MY_PARAM_2"]) require.Equal(t, "myval2", di["MY_PARAM_2"])
require.Equal(t, "/dev/foo/bar/devlink /dev/foo/bar/devlink1", di["DEVLINKS"]) require.Equal(t, "/dev/foo/bar/devlink /dev/foo/bar/devlink1", di["DEVLINKS"])
// test that data is cached
clean()
di, err = s.diskInfo("null")
require.NoError(t, err)
require.Equal(t, "myval1", di["MY_PARAM_1"])
require.Equal(t, "myval2", di["MY_PARAM_2"])
require.Equal(t, "/dev/foo/bar/devlink /dev/foo/bar/devlink1", di["DEVLINKS"])
// unfortunately we can't adjust mtime on /dev/null to test cache invalidation
} }
// DiskIOStats.diskName isn't a linux specific function, but dependent // DiskIOStats.diskName isn't a linux specific function, but dependent
@ -89,25 +46,39 @@ func TestDiskIOStats_diskName(t *testing.T) {
{[]string{"$MY_PARAM_2/$MISSING"}, "null"}, {[]string{"$MY_PARAM_2/$MISSING"}, "null"},
} }
for _, tc := range tests { for i, tc := range tests {
func() { t.Run(fmt.Sprintf("template %d", i), func(t *testing.T) {
s := DiskIO{ plugin := DiskIO{
NameTemplates: tc.templates, NameTemplates: tc.templates,
infoCache: map[string]diskInfoCache{
"null": {
modifiedAt: 0,
udevDataPath: "testdata/udev.txt",
sysBlockPath: "testdata",
values: map[string]string{},
},
},
} }
defer setupNullDisk(t, &s, "null")() //nolint:revive // done on purpose, cleaning will be executed properly name, _ := plugin.diskName("null")
name, _ := s.diskName("null")
require.Equal(t, tc.expected, name, "Templates: %#v", tc.templates) require.Equal(t, tc.expected, name, "Templates: %#v", tc.templates)
}() })
} }
} }
// DiskIOStats.diskTags isn't a linux specific function, but dependent // DiskIOStats.diskTags isn't a linux specific function, but dependent
// functions are a no-op on non-Linux. // functions are a no-op on non-Linux.
func TestDiskIOStats_diskTags(t *testing.T) { func TestDiskIOStats_diskTags(t *testing.T) {
s := &DiskIO{ plugin := &DiskIO{
DeviceTags: []string{"MY_PARAM_2"}, DeviceTags: []string{"MY_PARAM_2"},
infoCache: map[string]diskInfoCache{
"null": {
modifiedAt: 0,
udevDataPath: "testdata/udev.txt",
sysBlockPath: "testdata",
values: map[string]string{},
},
},
} }
defer setupNullDisk(t, s, "null")() //nolint:revive // done on purpose, cleaning will be executed properly dt := plugin.diskTags("null")
dt := s.diskTags("null")
require.Equal(t, map[string]string{"MY_PARAM_2": "myval2"}, dt) require.Equal(t, map[string]string{"MY_PARAM_2": "myval2"}, dt)
} }

View File

@ -2,24 +2,7 @@
package diskio package diskio
import ( type diskInfoCache struct{}
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/plugins/inputs/system"
)
type DiskIO struct {
ps system.PS
Devices []string
DeviceTags []string
NameTemplates []string
SkipSerialNumber bool
Log telegraf.Logger
deviceFilter filter.Filter
}
func (*DiskIO) diskInfo(_ string) (map[string]string, error) { func (*DiskIO) diskInfo(_ string) (map[string]string, error) {
return nil, nil return nil, nil

View File

@ -1,23 +1,19 @@
# Read metrics about disk IO by device # Read metrics about disk IO by device
[[inputs.diskio]] [[inputs.diskio]]
## By default, telegraf will gather stats for all devices including ## Devices to collect stats for
## disk partitions. ## Wildcards are supported except for disk synonyms like '/dev/disk/by-id'.
## Setting devices will restrict the stats to the specified devices. ## ex. devices = ["sda", "sdb", "vd*", "/dev/disk/by-id/nvme-eui.00123deadc0de123"]
## NOTE: Globbing expressions (e.g. asterix) are not supported for # devices = ["*"]
## disk synonyms like '/dev/disk/by-id'.
# devices = ["sda", "sdb", "vd*", "/dev/disk/by-id/nvme-eui.00123deadc0de123"] ## Skip gathering of the disk's serial numbers.
## Uncomment the following line if you need disk serial numbers. # skip_serial_number = true
# skip_serial_number = false
# ## Device metadata tags to add on systems supporting it (Linux only)
## On systems which support it, device metadata can be added in the form of ## Use 'udevadm info -q property -n <device>' to get a list of properties.
## 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'
## Note: Most, but not all, udev properties can be accessed this way. Properties ## Note: Most, but not all, udev properties can be accessed this way. Properties
## that are currently inaccessible include DEVTYPE, DEVNAME, and DEVPATH. ## that are currently inaccessible include DEVTYPE, DEVNAME, and DEVPATH.
# device_tags = ["ID_FS_TYPE", "ID_FS_USAGE"] # device_tags = ["ID_FS_TYPE", "ID_FS_USAGE"]
#
## Using the same metadata source as device_tags, you can also customize the ## Using the same metadata source as device_tags, you can also customize the
## name of the device via templates. ## name of the device via templates.
## The 'name_templates' parameter is a list of templates to try and apply to ## The 'name_templates' parameter is a list of templates to try and apply to

View File

@ -0,0 +1,4 @@
E:MY_PARAM_1=myval1
E:MY_PARAM_2=myval2
S:foo/bar/devlink
S:foo/bar/devlink1

5
plugins/inputs/diskio/testdata/uevent vendored Normal file
View File

@ -0,0 +1,5 @@
MAJOR=259
MINOR=1
DEVNAME=null
DEVTYPE=disk
DISKSEQ=2