feat: add Linux Volume Manager input plugin (#9771)
This commit is contained in:
parent
a9898f179b
commit
3eebfd2f0f
|
|
@ -1571,7 +1571,7 @@ func (c *Config) missingTomlField(_ reflect.Type, key string) error {
|
|||
"grok_timezone", "grok_unique_timestamp", "influx_max_line_bytes", "influx_sort_fields",
|
||||
"influx_uint_support", "interval", "json_name_key", "json_query", "json_strict",
|
||||
"json_string_fields", "json_time_format", "json_time_key", "json_timestamp_format", "json_timestamp_units", "json_timezone", "json_v2",
|
||||
"metric_batch_size", "metric_buffer_limit", "name_override", "name_prefix",
|
||||
"lvm", "metric_batch_size", "metric_buffer_limit", "name_override", "name_prefix",
|
||||
"name_suffix", "namedrop", "namepass", "order", "pass", "period", "precision",
|
||||
"prefix", "prometheus_export_timestamp", "prometheus_sort_metrics", "prometheus_string_as_label",
|
||||
"separator", "splunkmetric_hec_routing", "splunkmetric_multimetric", "tag_keys",
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ import (
|
|||
_ "github.com/influxdata/telegraf/plugins/inputs/logparser"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/logstash"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/lustre2"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/lvm"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/mailchimp"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/marklogic"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/mcrouter"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
# LVM Input Plugin
|
||||
|
||||
The Logical Volume Management (LVM) input plugin collects information about
|
||||
physical volumes, volume groups, and logical volumes.
|
||||
|
||||
### Configuration
|
||||
|
||||
The `lvm` command requires elevated permissions. If the user has configured
|
||||
sudo with the ability to run these commands, then set the `use_sudo` to true.
|
||||
|
||||
```toml
|
||||
# Read metrics about LVM physical volumes, volume groups, logical volumes.
|
||||
[[inputs.lvm]]
|
||||
## Use sudo to run LVM commands
|
||||
use_sudo = false
|
||||
```
|
||||
|
||||
#### Using sudo
|
||||
|
||||
If your account does not already have the ability to run commands
|
||||
with passwordless sudo then updates to the sudoers file are required. Below
|
||||
is an example to allow the requires LVM commands:
|
||||
|
||||
First, use the `visudo` command to start editing the sudoers file. Then add
|
||||
the following content, where `<username>` is the username of the user that
|
||||
needs this access:
|
||||
|
||||
```text
|
||||
Cmnd_Alias LVM = /usr/sbin/pvs *, /usr/sbin/vgs *, /usr/sbin/lvs *
|
||||
<username> ALL=(root) NOPASSWD: LVM
|
||||
Defaults!LVM !logfile, !syslog, !pam_session
|
||||
```
|
||||
|
||||
### Metrics
|
||||
|
||||
Metrics are broken out by physical volume (pv), volume group (vg), and logical
|
||||
volume (lv):
|
||||
|
||||
- lvm_physical_vol
|
||||
- tags
|
||||
- path
|
||||
- vol_group
|
||||
- fields
|
||||
- size
|
||||
- free
|
||||
- used
|
||||
- used_percent
|
||||
- lvm_vol_group
|
||||
- tags
|
||||
- name
|
||||
- fields
|
||||
- size
|
||||
- free
|
||||
- used_percent
|
||||
- physical_volume_count
|
||||
- logical_volume_count
|
||||
- snapshot_count
|
||||
- lvm_logical_vol
|
||||
- tags
|
||||
- name
|
||||
- vol_group
|
||||
- fields
|
||||
- size
|
||||
- data_percent
|
||||
- meta_percent
|
||||
|
||||
### Example Output
|
||||
|
||||
The following example shows a system with the root partition on an LVM group
|
||||
as well as with a Docker thin-provisioned LVM group on a second drive:
|
||||
|
||||
> lvm_physical_vol,path=/dev/sda2,vol_group=vgroot free=0i,size=249510756352i,used=249510756352i,used_percent=100 1631823026000000000
|
||||
> lvm_physical_vol,path=/dev/sdb,vol_group=docker free=3858759680i,size=128316342272i,used=124457582592i,used_percent=96.99277612525741 1631823026000000000
|
||||
> lvm_vol_group,name=vgroot free=0i,logical_volume_count=1i,physical_volume_count=1i,size=249510756352i,snapshot_count=0i,used_percent=100 1631823026000000000
|
||||
> lvm_vol_group,name=docker free=3858759680i,logical_volume_count=1i,physical_volume_count=1i,size=128316342272i,snapshot_count=0i,used_percent=96.99277612525741 1631823026000000000
|
||||
> lvm_logical_vol,name=lvroot,vol_group=vgroot data_percent=0,metadata_percent=0,size=249510756352i 1631823026000000000
|
||||
> lvm_logical_vol,name=thinpool,vol_group=docker data_percent=0.36000001430511475,metadata_percent=1.3300000429153442,size=121899057152i 1631823026000000000
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
package lvm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
var (
|
||||
execCommand = exec.Command
|
||||
)
|
||||
|
||||
var sampleConfig = `
|
||||
## Use sudo to run LVM commands
|
||||
use_sudo = false
|
||||
`
|
||||
|
||||
type LVM struct {
|
||||
UseSudo bool `toml:"use_sudo"`
|
||||
}
|
||||
|
||||
func (lvm *LVM) Description() string {
|
||||
return "Read metrics about LVM physical volumes, volume groups, logical volumes."
|
||||
}
|
||||
|
||||
func (lvm *LVM) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (lvm *LVM) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lvm *LVM) Gather(acc telegraf.Accumulator) error {
|
||||
if err := lvm.gatherPhysicalVolumes(acc); err != nil {
|
||||
return err
|
||||
} else if err := lvm.gatherVolumeGroups(acc); err != nil {
|
||||
return err
|
||||
} else if err := lvm.gatherLogicalVolumes(acc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lvm *LVM) gatherPhysicalVolumes(acc telegraf.Accumulator) error {
|
||||
pvsCmd := "/usr/sbin/pvs"
|
||||
args := []string{
|
||||
"--reportformat", "json", "--units", "b", "--nosuffix",
|
||||
"-o", "pv_name,vg_name,pv_size,pv_free,pv_used",
|
||||
}
|
||||
out, err := lvm.runCmd(pvsCmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var report pvsReport
|
||||
err = json.Unmarshal(out, &report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal physical volume JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(report.Report) > 0 {
|
||||
for _, pv := range report.Report[0].Pv {
|
||||
tags := map[string]string{
|
||||
"path": pv.Name,
|
||||
"vol_group": pv.VolGroup,
|
||||
}
|
||||
|
||||
size, err := strconv.ParseUint(pv.Size, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
free, err := strconv.ParseUint(pv.Free, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
used, err := strconv.ParseUint(pv.Used, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usedPercent := float64(used) / float64(size) * 100
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"size": size,
|
||||
"free": free,
|
||||
"used": used,
|
||||
"used_percent": usedPercent,
|
||||
}
|
||||
|
||||
acc.AddFields("lvm_physical_vol", fields, tags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lvm *LVM) gatherVolumeGroups(acc telegraf.Accumulator) error {
|
||||
cmd := "/usr/sbin/vgs"
|
||||
args := []string{
|
||||
"--reportformat", "json", "--units", "b", "--nosuffix",
|
||||
"-o", "vg_name,pv_count,lv_count,snap_count,vg_size,vg_free",
|
||||
}
|
||||
out, err := lvm.runCmd(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var report vgsReport
|
||||
err = json.Unmarshal(out, &report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal vol group JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(report.Report) > 0 {
|
||||
for _, vg := range report.Report[0].Vg {
|
||||
tags := map[string]string{
|
||||
"name": vg.Name,
|
||||
}
|
||||
|
||||
size, err := strconv.ParseUint(vg.Size, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
free, err := strconv.ParseUint(vg.Free, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pvCount, err := strconv.ParseUint(vg.PvCount, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lvCount, err := strconv.ParseUint(vg.LvCount, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapCount, err := strconv.ParseUint(vg.SnapCount, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usedPercent := (float64(size) - float64(free)) / float64(size) * 100
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"size": size,
|
||||
"free": free,
|
||||
"used_percent": usedPercent,
|
||||
"physical_volume_count": pvCount,
|
||||
"logical_volume_count": lvCount,
|
||||
"snapshot_count": snapCount,
|
||||
}
|
||||
|
||||
acc.AddFields("lvm_vol_group", fields, tags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lvm *LVM) gatherLogicalVolumes(acc telegraf.Accumulator) error {
|
||||
cmd := "/usr/sbin/lvs"
|
||||
args := []string{
|
||||
"--reportformat", "json", "--units", "b", "--nosuffix",
|
||||
"-o", "lv_name,vg_name,lv_size,data_percent,metadata_percent",
|
||||
}
|
||||
out, err := lvm.runCmd(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var report lvsReport
|
||||
err = json.Unmarshal(out, &report)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal logical vol JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(report.Report) > 0 {
|
||||
for _, lv := range report.Report[0].Lv {
|
||||
tags := map[string]string{
|
||||
"name": lv.Name,
|
||||
"vol_group": lv.VolGroup,
|
||||
}
|
||||
|
||||
size, err := strconv.ParseUint(lv.Size, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Does not apply to all logical volumes, set default value
|
||||
if lv.DataPercent == "" {
|
||||
lv.DataPercent = "0.0"
|
||||
}
|
||||
dataPercent, err := strconv.ParseFloat(lv.DataPercent, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Does not apply to all logical volumes, set default value
|
||||
if lv.MetadataPercent == "" {
|
||||
lv.MetadataPercent = "0.0"
|
||||
}
|
||||
metadataPercent, err := strconv.ParseFloat(lv.MetadataPercent, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"size": size,
|
||||
"data_percent": dataPercent,
|
||||
"metadata_percent": metadataPercent,
|
||||
}
|
||||
|
||||
acc.AddFields("lvm_logical_vol", fields, tags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lvm *LVM) runCmd(cmd string, args []string) ([]byte, error) {
|
||||
execCmd := execCommand(cmd, args...)
|
||||
if lvm.UseSudo {
|
||||
execCmd = execCommand("sudo", append([]string{"-n", cmd}, args...)...)
|
||||
}
|
||||
|
||||
out, err := internal.StdOutputTimeout(execCmd, 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to run command %s: %s - %s",
|
||||
strings.Join(execCmd.Args, " "), err, string(out),
|
||||
)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Represents info about physical volume command, pvs, output
|
||||
type pvsReport struct {
|
||||
Report []struct {
|
||||
Pv []struct {
|
||||
Name string `json:"pv_name"`
|
||||
VolGroup string `json:"vg_name"`
|
||||
Size string `json:"pv_size"`
|
||||
Free string `json:"pv_free"`
|
||||
Used string `json:"pv_used"`
|
||||
} `json:"pv"`
|
||||
} `json:"report"`
|
||||
}
|
||||
|
||||
// Represents info about volume group command, vgs, output
|
||||
type vgsReport struct {
|
||||
Report []struct {
|
||||
Vg []struct {
|
||||
Name string `json:"vg_name"`
|
||||
Size string `json:"vg_size"`
|
||||
Free string `json:"vg_free"`
|
||||
LvCount string `json:"lv_count"`
|
||||
PvCount string `json:"pv_count"`
|
||||
SnapCount string `json:"snap_count"`
|
||||
} `json:"vg"`
|
||||
} `json:"report"`
|
||||
}
|
||||
|
||||
// Represents info about logical volume command, lvs, output
|
||||
type lvsReport struct {
|
||||
Report []struct {
|
||||
Lv []struct {
|
||||
Name string `json:"lv_name"`
|
||||
VolGroup string `json:"vg_name"`
|
||||
Size string `json:"lv_size"`
|
||||
DataPercent string `json:"data_percent"`
|
||||
MetadataPercent string `json:"metadata_percent"`
|
||||
} `json:"lv"`
|
||||
} `json:"report"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("lvm", func() telegraf.Input {
|
||||
return &LVM{}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package lvm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
var lvm LVM = LVM{UseSudo: false}
|
||||
var acc testutil.Accumulator
|
||||
|
||||
// overwriting exec commands with mock commands
|
||||
execCommand = fakeExecCommand
|
||||
err := lvm.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
pvsTags := map[string]string{
|
||||
"path": "/dev/sdb",
|
||||
"vol_group": "docker",
|
||||
}
|
||||
pvsFields := map[string]interface{}{
|
||||
"size": uint64(128316342272),
|
||||
"free": uint64(3858759680),
|
||||
"used": uint64(124457582592),
|
||||
"used_percent": 96.99277612525741,
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "lvm_physical_vol", pvsFields, pvsTags)
|
||||
|
||||
vgsTags := map[string]string{
|
||||
"name": "docker",
|
||||
}
|
||||
vgsFields := map[string]interface{}{
|
||||
"size": uint64(128316342272),
|
||||
"free": uint64(3858759680),
|
||||
"used_percent": 96.99277612525741,
|
||||
"physical_volume_count": uint64(1),
|
||||
"logical_volume_count": uint64(1),
|
||||
"snapshot_count": uint64(0),
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "lvm_vol_group", vgsFields, vgsTags)
|
||||
|
||||
lvsTags := map[string]string{
|
||||
"name": "thinpool",
|
||||
"vol_group": "docker",
|
||||
}
|
||||
lvsFields := map[string]interface{}{
|
||||
"size": uint64(121899057152),
|
||||
"data_percent": 0.36000001430511475,
|
||||
"metadata_percent": 1.3300000429153442,
|
||||
}
|
||||
acc.AssertContainsTaggedFields(t, "lvm_logical_vol", lvsFields, lvsTags)
|
||||
}
|
||||
|
||||
// Used as a helper function that mock the exec.Command call
|
||||
func fakeExecCommand(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcess", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Used to mock exec.Command output
|
||||
func TestHelperProcess(_ *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
mockPVSData := `{
|
||||
"report": [
|
||||
{
|
||||
"pv": [
|
||||
{"pv_name":"/dev/sdb", "vg_name":"docker", "pv_size":"128316342272", "pv_free":"3858759680", "pv_used":"124457582592"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
mockVGSData := `{
|
||||
"report": [
|
||||
{
|
||||
"vg": [
|
||||
{"vg_name":"docker", "pv_count":"1", "lv_count":"1", "snap_count":"0", "vg_size":"128316342272", "vg_free":"3858759680"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
mockLVSData := `{
|
||||
"report": [
|
||||
{
|
||||
"lv": [
|
||||
{"lv_name":"thinpool", "vg_name":"docker", "lv_size":"121899057152", "data_percent":"0.36", "metadata_percent":"1.33"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
// Previous arguments are tests stuff, that looks like :
|
||||
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
|
||||
args := os.Args
|
||||
cmd := args[3]
|
||||
if cmd == "/usr/sbin/pvs" {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, mockPVSData)
|
||||
} else if cmd == "/usr/sbin/vgs" {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, mockVGSData)
|
||||
} else if cmd == "/usr/sbin/lvs" {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, mockLVSData)
|
||||
} else {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, "command not found")
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(1)
|
||||
}
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// test when no lvm devices exist
|
||||
func TestGatherNoLVM(t *testing.T) {
|
||||
var noLVM LVM = LVM{UseSudo: false}
|
||||
var acc testutil.Accumulator
|
||||
|
||||
// overwriting exec commands with mock commands
|
||||
execCommand = fakeExecCommandNoLVM
|
||||
err := noLVM.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
acc.AssertDoesNotContainMeasurement(t, "lvm_physical_vol")
|
||||
acc.AssertDoesNotContainMeasurement(t, "lvm_vol_group")
|
||||
acc.AssertDoesNotContainMeasurement(t, "lvm_logical_vol")
|
||||
}
|
||||
|
||||
// Used as a helper function that mock the exec.Command call
|
||||
func fakeExecCommandNoLVM(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcessNoLVM", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Used to mock exec.Command output
|
||||
func TestHelperProcessNoLVM(_ *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
mockPVSData := `{
|
||||
"report": [
|
||||
{
|
||||
"pv": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
mockVGSData := `{
|
||||
"report": [
|
||||
{
|
||||
"vg": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
mockLVSData := `{
|
||||
"report": [
|
||||
{
|
||||
"lv": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
// Previous arguments are tests stuff, that looks like :
|
||||
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
|
||||
args := os.Args
|
||||
cmd := args[3]
|
||||
if cmd == "/usr/sbin/pvs" {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, mockPVSData)
|
||||
} else if cmd == "/usr/sbin/vgs" {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, mockVGSData)
|
||||
} else if cmd == "/usr/sbin/lvs" {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, mockLVSData)
|
||||
} else {
|
||||
//nolint:errcheck,revive // test will fail anyway
|
||||
fmt.Fprint(os.Stdout, "command not found")
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(1)
|
||||
}
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(0)
|
||||
}
|
||||
Loading…
Reference in New Issue