add simple collector to monitor lvm thin volumes space usage (#17394)
* add lvs to ndsudo * simple collector to monitor lvm thin volumes space usage * allow comma in ndsudo params * fixes * enable by default * add this pool check for now
Ilya Mashchenko committed
Apr 12, 2024 at 20:42 UTC
2a1f4e8ee7ba4cd89a0a05d55029db953c2fc23b
19 files changed
+809
-5
src/collectors/plugins.d/ndsudo.c
+10
-2
@@ -13,7 +13,15 @@ struct command {
13
const char *params;
14
const char *search[MAX_SEARCH];
15
} allowed_commands[] = {
16
- {
16
+ {
17
+ .name = "lvs-report-json",
18
+ .params = "--reportformat json --units b --nosuffix -o {{options}}",
19
+ .search = {
20
+ [0] = "lvs",
21
+ [1] = NULL,
22
+ },
23
+ },
24
+ {
25
.name = "igt-json",
26
.params = "-J -s {{interval}}",
27
.search = {
@@ -117,7 +125,7 @@ bool check_string(const char *str, size_t index, char *err, size_t err_size) {
125
if(!((c >= 'A' && c <= 'Z') ||
126
(c >= 'a' && c <= 'z') ||
127
(c >= '0' && c <= '9') ||
120
- c == ' ' || c == '_' || c == '-' || c == '/' || c == '.')) {
128
+ c == ' ' || c == '_' || c == '-' || c == '/' || c == '.' || c == ',')) {
129
snprintf(err, err_size, "command line argument No %zu includes invalid character '%c'", index, c);
130
return false;
131
}
src/go/collectors/go.d.plugin/README.md
+1
@@ -85,6 +85,7 @@ see the appropriate collector readme.
85
| [lighttpd](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/lighttpd) | Lighttpd |
86
| [logind](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/logind) | systemd-logind |
87
| [logstash](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/logstash) | Logstash |
88
+| [lvm](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/lvm) | LVM logical volumes |
89
| [mongoDB](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/mongodb) | MongoDB |
90
| [mysql](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/mysql) | MySQL |
91
| [nginx](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/nginx) | NGINX |
src/go/collectors/go.d.plugin/config/go.d.conf
+1
@@ -47,6 +47,7 @@ modules:
47
# lighttpd: yes
48
# logind: yes
49
# logstash: yes
50
+# lvm: yes
51
# mongodb: yes
52
# mysql: yes
53
# nginx: yes
src/go/collectors/go.d.plugin/config/go.d/lvm.conf
new
+5
@@ -0,0 +1,5 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/lvm#readme
3
+
4
+jobs:
5
+ - name: lvm
src/go/collectors/go.d.plugin/modules/init.go
+1
@@ -38,6 +38,7 @@ import (
38
_ "github.com/netdata/netdata/go/go.d.plugin/modules/lighttpd"
39
_ "github.com/netdata/netdata/go/go.d.plugin/modules/logind"
40
_ "github.com/netdata/netdata/go/go.d.plugin/modules/logstash"
41
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/lvm"
42
_ "github.com/netdata/netdata/go/go.d.plugin/modules/mongodb"
43
_ "github.com/netdata/netdata/go/go.d.plugin/modules/mysql"
44
_ "github.com/netdata/netdata/go/go.d.plugin/modules/nginx"
src/go/collectors/go.d.plugin/modules/lvm/charts.go
new
+66
@@ -0,0 +1,66 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package lvm
4
+
5
+import (
6
+ "fmt"
7
+
8
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
9
+)
10
+
11
+const (
12
+ prioLVDataPercent = 2920 + iota
13
+ prioLVMetadataPercent
14
+)
15
+
16
+var lvThinPoolChartsTmpl = module.Charts{
17
+ lvDataSpaceUtilizationChartTmpl.Copy(),
18
+ lvMetadataSpaceUtilizationChartTmpl.Copy(),
19
+}
20
+
21
+var (
22
+ lvDataSpaceUtilizationChartTmpl = module.Chart{
23
+ ID: "lv_%s_vg_%s_lv_data_space_utilization",
24
+ Title: "Logical volume space allocated for data",
25
+ Units: "percentage",
26
+ Fam: "lv space usage",
27
+ Ctx: "lvm.lv_data_space_utilization",
28
+ Type: module.Area,
29
+ Priority: prioLVDataPercent,
30
+ Dims: module.Dims{
31
+ {ID: "lv_%s_vg_%s_data_percent", Name: "utilization", Div: 100},
32
+ },
33
+ }
34
+ lvMetadataSpaceUtilizationChartTmpl = module.Chart{
35
+ ID: "lv_%s_vg_%s_lv_metadata_space_utilization",
36
+ Title: "Logical volume space allocated for metadata",
37
+ Units: "percentage",
38
+ Fam: "lv space usage",
39
+ Ctx: "lvm.lv_metadata_space_utilization",
40
+ Type: module.Area,
41
+ Priority: prioLVMetadataPercent,
42
+ Dims: module.Dims{
43
+ {ID: "lv_%s_vg_%s_metadata_percent", Name: "utilization", Div: 100},
44
+ },
45
+ }
46
+)
47
+
48
+func (l *LVM) addLVMThinPoolCharts(lvName, vgName string) {
49
+ charts := lvThinPoolChartsTmpl.Copy()
50
+
51
+ for _, chart := range *charts {
52
+ chart.ID = fmt.Sprintf(chart.ID, lvName, vgName)
53
+ chart.Labels = []module.Label{
54
+ {Key: "lv_name", Value: lvName},
55
+ {Key: "vg_name", Value: vgName},
56
+ {Key: "volume_type", Value: "thin_pool"},
57
+ }
58
+ for _, dim := range chart.Dims {
59
+ dim.ID = fmt.Sprintf(dim.ID, lvName, vgName)
60
+ }
61
+ }
62
+
63
+ if err := l.Charts().Add(*charts...); err != nil {
64
+ l.Warning(err)
65
+ }
66
+}
src/go/collectors/go.d.plugin/modules/lvm/collect.go
new
+131
@@ -0,0 +1,131 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package lvm
4
+
5
+import (
6
+ "encoding/json"
7
+ "fmt"
8
+ "strconv"
9
+)
10
+
11
+type lvsReport struct {
12
+ Report []struct {
13
+ Lv []struct {
14
+ VGName string `json:"vg_name"`
15
+ LVName string `json:"lv_name"`
16
+ LVSize string `json:"lv_size"`
17
+ DataPercent string `json:"data_percent"`
18
+ MetadataPercent string `json:"metadata_percent"`
19
+ LVAttr string `json:"lv_attr"`
20
+ } `json:"lv"`
21
+ } `json:"report"`
22
+}
23
+
24
+func (l *LVM) collect() (map[string]int64, error) {
25
+ bs, err := l.exec.lvsReportJson()
26
+ if err != nil {
27
+ return nil, err
28
+ }
29
+
30
+ var report lvsReport
31
+ if err = json.Unmarshal(bs, &report); err != nil {
32
+ return nil, err
33
+ }
34
+
35
+ mx := make(map[string]int64)
36
+
37
+ for _, r := range report.Report {
38
+ for _, lv := range r.Lv {
39
+ if lv.VGName == "" || lv.LVName == "" {
40
+ continue
41
+ }
42
+
43
+ if !isThinPool(lv.LVAttr) {
44
+ l.Debugf("skipping lv '%s' vg '%s': not a thin pool", lv.LVName, lv.VGName)
45
+ continue
46
+ }
47
+
48
+ key := fmt.Sprintf("lv_%s_vg_%s", lv.LVName, lv.VGName)
49
+ if !l.lvmThinPools[key] {
50
+ l.addLVMThinPoolCharts(lv.LVName, lv.VGName)
51
+ l.lvmThinPools[key] = true
52
+ }
53
+ if v, ok := parseFloat(lv.DataPercent); ok {
54
+ mx[key+"_data_percent"] = int64(v * 100)
55
+ }
56
+ if v, ok := parseFloat(lv.MetadataPercent); ok {
57
+ mx[key+"_metadata_percent"] = int64(v * 100)
58
+ }
59
+ }
60
+ }
61
+
62
+ return mx, nil
63
+}
64
+
65
+func isThinPool(lvAttr string) bool {
66
+ return getLVType(lvAttr) == "thin_pool"
67
+}
68
+
69
+func getLVType(lvAttr string) string {
70
+ if len(lvAttr) == 0 {
71
+ return ""
72
+ }
73
+
74
+ // https://man7.org/linux/man-pages/man8/lvs.8.html#NOTES
75
+ switch lvAttr[0] {
76
+ case 'C':
77
+ return "cache"
78
+ case 'm':
79
+ return "mirrored"
80
+ case 'M':
81
+ return "mirrored_without_initial_sync"
82
+ case 'o':
83
+ return "origin"
84
+ case 'O':
85
+ return "origin_with_merging_snapshot"
86
+ case 'g':
87
+ return "integrity"
88
+ case 'r':
89
+ return "raid"
90
+ case 'R':
91
+ return "raid_without_initial_sync"
92
+ case 's':
93
+ return "snapshot"
94
+ case 'S':
95
+ return "merging_snapshot"
96
+ case 'p':
97
+ return "pvmove"
98
+ case 'v':
99
+ return "virtual"
100
+ case 'i':
101
+ return "mirror_or_raid_image"
102
+ case 'I':
103
+ return "mirror_or_raid_mage_out_of_sync"
104
+ case 'l':
105
+ return "log_device"
106
+ case 'c':
107
+ return "under_conversion"
108
+ case 'V':
109
+ return "thin_volume"
110
+ case 't':
111
+ return "thin_pool"
112
+ case 'T':
113
+ return "thin_pool_data"
114
+ case 'd':
115
+ return "vdo_pool"
116
+ case 'D':
117
+ return "vdo_pool_data"
118
+ case 'e':
119
+ return "raid_or_pool_metadata"
120
+ default:
121
+ return ""
122
+ }
123
+}
124
+
125
+func parseFloat(s string) (float64, bool) {
126
+ if s == "-" {
127
+ return 0, false
128
+ }
129
+ v, err := strconv.ParseFloat(s, 64)
130
+ return v, err == nil
131
+}
src/go/collectors/go.d.plugin/modules/lvm/config_schema.json
new
+35
@@ -0,0 +1,35 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "LVM collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 10
13
+ },
14
+ "timeout": {
15
+ "title": "Timeout",
16
+ "description": "Timeout for executing the binary, specified in seconds.",
17
+ "type": "number",
18
+ "minimum": 0.5,
19
+ "default": 2
20
+ }
21
+ },
22
+ "additionalProperties": false,
23
+ "patternProperties": {
24
+ "^name$": {}
25
+ }
26
+ },
27
+ "uiSchema": {
28
+ "uiOptions": {
29
+ "fullPage": true
30
+ },
31
+ "timeout": {
32
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
33
+ }
34
+ }
35
+}
src/go/collectors/go.d.plugin/modules/lvm/exec.go
new
+47
@@ -0,0 +1,47 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package lvm
4
+
5
+import (
6
+ "context"
7
+ "fmt"
8
+ "os/exec"
9
+ "time"
10
+
11
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
12
+)
13
+
14
+func newLVMCLIExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *lvmCLIExec {
15
+ return &lvmCLIExec{
16
+ Logger: log,
17
+ ndsudoPath: ndsudoPath,
18
+ timeout: timeout,
19
+ }
20
+}
21
+
22
+type lvmCLIExec struct {
23
+ *logger.Logger
24
+
25
+ ndsudoPath string
26
+ timeout time.Duration
27
+}
28
+
29
+func (e *lvmCLIExec) lvsReportJson() ([]byte, error) {
30
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
31
+ defer cancel()
32
+
33
+ cmd := exec.CommandContext(ctx,
34
+ e.ndsudoPath,
35
+ "lvs-report-json",
36
+ "--options",
37
+ "vg_name,lv_name,lv_size,data_percent,metadata_percent,lv_attr",
38
+ )
39
+ e.Debugf("executing '%s'", cmd)
40
+
41
+ bs, err := cmd.Output()
42
+ if err != nil {
43
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
44
+ }
45
+
46
+ return bs, nil
47
+}
src/go/collectors/go.d.plugin/modules/lvm/init.go
new
+23
@@ -0,0 +1,23 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package lvm
4
+
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/executable"
11
+)
12
+
13
+func (l *LVM) initLVMCLIExec() (lvmCLI, error) {
14
+ ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15
+ if _, err := os.Stat(ndsudoPath); err != nil {
16
+ return nil, fmt.Errorf("ndsudo executable not found: %v", err)
17
+
18
+ }
19
+
20
+ lvmExec := newLVMCLIExec(ndsudoPath, l.Timeout.Duration(), l.Logger)
21
+
22
+ return lvmExec, nil
23
+}
src/go/collectors/go.d.plugin/modules/lvm/lvm.go
new
+104
@@ -0,0 +1,104 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package lvm
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
11
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
12
+)
13
+
14
+//go:embed "config_schema.json"
15
+var configSchema string
16
+
17
+func init() {
18
+ module.Register("lvm", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Defaults: module.Defaults{
21
+ UpdateEvery: 10,
22
+ },
23
+ Create: func() module.Module { return New() },
24
+ })
25
+}
26
+
27
+func New() *LVM {
28
+ return &LVM{
29
+ Config: Config{
30
+ Timeout: web.Duration(time.Second * 2),
31
+ },
32
+ charts: &module.Charts{},
33
+ lvmThinPools: make(map[string]bool),
34
+ }
35
+}
36
+
37
+type Config struct {
38
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
39
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
40
+}
41
+
42
+type (
43
+ LVM struct {
44
+ module.Base
45
+ Config `yaml:",inline" json:""`
46
+
47
+ charts *module.Charts
48
+
49
+ exec lvmCLI
50
+
51
+ lvmThinPools map[string]bool
52
+ }
53
+ lvmCLI interface {
54
+ lvsReportJson() ([]byte, error)
55
+ }
56
+)
57
+
58
+func (l *LVM) Configuration() any {
59
+ return l.Config
60
+}
61
+
62
+func (l *LVM) Init() error {
63
+ lvmExec, err := l.initLVMCLIExec()
64
+ if err != nil {
65
+ l.Errorf("lvm exec initialization: %v", err)
66
+ return err
67
+ }
68
+ l.exec = lvmExec
69
+
70
+ return nil
71
+}
72
+
73
+func (l *LVM) Check() error {
74
+ mx, err := l.collect()
75
+ if err != nil {
76
+ l.Error(err)
77
+ return err
78
+ }
79
+
80
+ if len(mx) == 0 {
81
+ return errors.New("no metrics collected")
82
+ }
83
+
84
+ return nil
85
+}
86
+
87
+func (l *LVM) Charts() *module.Charts {
88
+ return l.charts
89
+}
90
+
91
+func (l *LVM) Collect() map[string]int64 {
92
+ mx, err := l.collect()
93
+ if err != nil {
94
+ l.Error(err)
95
+ }
96
+
97
+ if len(mx) == 0 {
98
+ return nil
99
+ }
100
+
101
+ return mx
102
+}
103
+
104
+func (l *LVM) Cleanup() {}
src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go
new
+239
@@ -0,0 +1,239 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package lvm
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "testing"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
11
+
12
+ "github.com/stretchr/testify/assert"
13
+ "github.com/stretchr/testify/require"
14
+)
15
+
16
+var (
17
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19
+
20
+ dataLvsReportJson, _ = os.ReadFile("testdata/lvs-report.json")
21
+ dataLvsReportNoThinJson, _ = os.ReadFile("testdata/lvs-report-no-thin.json")
22
+)
23
+
24
+func Test_testDataIsValid(t *testing.T) {
25
+ for name, data := range map[string][]byte{
26
+ "dataConfigJSON": dataConfigJSON,
27
+ "dataConfigYAML": dataConfigYAML,
28
+
29
+ "dataLvsReportJson": dataLvsReportJson,
30
+ "dataLvsReportNoThinJson": dataLvsReportNoThinJson,
31
+ } {
32
+ require.NotNil(t, data, name)
33
+
34
+ }
35
+}
36
+
37
+func TestLVM_Configuration(t *testing.T) {
38
+ module.TestConfigurationSerialize(t, &LVM{}, dataConfigJSON, dataConfigYAML)
39
+}
40
+
41
+func TestLVM_Init(t *testing.T) {
42
+ tests := map[string]struct {
43
+ config Config
44
+ wantFail bool
45
+ }{
46
+ "fails if failed to locate ndsudo": {
47
+ wantFail: true,
48
+ config: New().Config,
49
+ },
50
+ }
51
+
52
+ for name, test := range tests {
53
+ t.Run(name, func(t *testing.T) {
54
+ lvm := New()
55
+ lvm.Config = test.config
56
+
57
+ if test.wantFail {
58
+ assert.Error(t, lvm.Init())
59
+ } else {
60
+ assert.NoError(t, lvm.Init())
61
+ }
62
+ })
63
+ }
64
+
65
+}
66
+
67
+func TestLVM_Cleanup(t *testing.T) {
68
+ tests := map[string]struct {
69
+ prepare func() *LVM
70
+ }{
71
+ "not initialized exec": {
72
+ prepare: func() *LVM {
73
+ return New()
74
+ },
75
+ },
76
+ "after check": {
77
+ prepare: func() *LVM {
78
+ lvm := New()
79
+ lvm.exec = prepareMockOK()
80
+ _ = lvm.Check()
81
+ return lvm
82
+ },
83
+ },
84
+ "after collect": {
85
+ prepare: func() *LVM {
86
+ lvm := New()
87
+ lvm.exec = prepareMockOK()
88
+ _ = lvm.Collect()
89
+ return lvm
90
+ },
91
+ },
92
+ }
93
+
94
+ for name, test := range tests {
95
+ t.Run(name, func(t *testing.T) {
96
+ lvm := test.prepare()
97
+
98
+ assert.NotPanics(t, lvm.Cleanup)
99
+ })
100
+ }
101
+}
102
+
103
+func TestLVM_Charts(t *testing.T) {
104
+ assert.NotNil(t, New().Charts())
105
+}
106
+
107
+func TestLVM_Check(t *testing.T) {
108
+ tests := map[string]struct {
109
+ prepareMock func() *mockLvmCliExec
110
+ wantFail bool
111
+ }{
112
+ "success case": {
113
+ prepareMock: prepareMockOK,
114
+ wantFail: false,
115
+ },
116
+ "no thin volumes": {
117
+ prepareMock: prepareMockNoThinVolumes,
118
+ wantFail: true,
119
+ },
120
+ "error on lvs report call": {
121
+ prepareMock: prepareMockErrOnLvsReportJson,
122
+ wantFail: true,
123
+ },
124
+ "empty response": {
125
+ prepareMock: prepareMockEmptyResponse,
126
+ wantFail: true,
127
+ },
128
+ "unexpected response": {
129
+ prepareMock: prepareMockUnexpectedResponse,
130
+ wantFail: true,
131
+ },
132
+ }
133
+
134
+ for name, test := range tests {
135
+ t.Run(name, func(t *testing.T) {
136
+ lvm := New()
137
+ mock := test.prepareMock()
138
+ lvm.exec = mock
139
+
140
+ if test.wantFail {
141
+ assert.Error(t, lvm.Check())
142
+ } else {
143
+ assert.NoError(t, lvm.Check())
144
+ }
145
+ })
146
+ }
147
+}
148
+
149
+func TestLVM_Collect(t *testing.T) {
150
+ tests := map[string]struct {
151
+ prepareMock func() *mockLvmCliExec
152
+ wantMetrics map[string]int64
153
+ }{
154
+ "success case": {
155
+ prepareMock: prepareMockOK,
156
+ wantMetrics: map[string]int64{
157
+ "lv_root_vg_cm-vg_data_percent": 7889,
158
+ "lv_root_vg_cm-vg_metadata_percent": 1925,
159
+ },
160
+ },
161
+ "no thin volumes": {
162
+ prepareMock: prepareMockNoThinVolumes,
163
+ wantMetrics: nil,
164
+ },
165
+ "error on lvs report call": {
166
+ prepareMock: prepareMockErrOnLvsReportJson,
167
+ wantMetrics: nil,
168
+ },
169
+ "empty response": {
170
+ prepareMock: prepareMockEmptyResponse,
171
+ wantMetrics: nil,
172
+ },
173
+ "unexpected response": {
174
+ prepareMock: prepareMockUnexpectedResponse,
175
+ wantMetrics: nil,
176
+ },
177
+ }
178
+
179
+ for name, test := range tests {
180
+ t.Run(name, func(t *testing.T) {
181
+ lvm := New()
182
+ mock := test.prepareMock()
183
+ lvm.exec = mock
184
+
185
+ mx := lvm.Collect()
186
+
187
+ assert.Equal(t, test.wantMetrics, mx)
188
+ if len(test.wantMetrics) > 0 {
189
+ assert.Len(t, *lvm.Charts(), len(lvThinPoolChartsTmpl)*len(lvm.lvmThinPools))
190
+ }
191
+ })
192
+ }
193
+
194
+}
195
+
196
+func prepareMockOK() *mockLvmCliExec {
197
+ return &mockLvmCliExec{
198
+ lvsReportJsonData: dataLvsReportJson,
199
+ }
200
+}
201
+
202
+func prepareMockNoThinVolumes() *mockLvmCliExec {
203
+ return &mockLvmCliExec{
204
+ lvsReportJsonData: dataLvsReportNoThinJson,
205
+ }
206
+}
207
+
208
+func prepareMockErrOnLvsReportJson() *mockLvmCliExec {
209
+ return &mockLvmCliExec{
210
+ errOnLvsReportJson: true,
211
+ }
212
+}
213
+
214
+func prepareMockEmptyResponse() *mockLvmCliExec {
215
+ return &mockLvmCliExec{}
216
+}
217
+
218
+func prepareMockUnexpectedResponse() *mockLvmCliExec {
219
+ return &mockLvmCliExec{
220
+ lvsReportJsonData: []byte(`
221
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
222
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
223
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
224
+`),
225
+ }
226
+}
227
+
228
+type mockLvmCliExec struct {
229
+ errOnLvsReportJson bool
230
+ lvsReportJsonData []byte
231
+}
232
+
233
+func (m *mockLvmCliExec) lvsReportJson() ([]byte, error) {
234
+ if m.errOnLvsReportJson {
235
+ return nil, errors.New("mock.lvsReportJson() error")
236
+ }
237
+
238
+ return m.lvsReportJsonData, nil
239
+}
src/go/collectors/go.d.plugin/modules/lvm/metadata.yaml
new
+107
@@ -0,0 +1,107 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-lvm
5
+ plugin_name: go.d.plugin
6
+ module_name: lvm
7
+ monitored_instance:
8
+ name: LVM logical volumes
9
+ link: ""
10
+ icon_filename: filesystem.svg
11
+ categories:
12
+ - data-collection.storage-mount-points-and-filesystems
13
+ keywords:
14
+ - lvm
15
+ - lvs
16
+ related_resources:
17
+ integrations:
18
+ list: []
19
+ info_provided_to_referring_integrations:
20
+ description: ""
21
+ most_popular: false
22
+ overview:
23
+ data_collection:
24
+ metrics_description: >
25
+ This collector monitors the health of LVM logical volumes.
26
+ It relies on the [`lvs`](https://man7.org/linux/man-pages/man8/lvs.8.html) CLI tool but avoids directly executing the binary.
27
+ Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment.
28
+ This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management.
29
+ method_description: ""
30
+ supported_platforms:
31
+ include: []
32
+ exclude: []
33
+ multi_instance: false
34
+ additional_permissions:
35
+ description: ""
36
+ default_behavior:
37
+ auto_detection:
38
+ description: ""
39
+ limits:
40
+ description: ""
41
+ performance_impact:
42
+ description: ""
43
+ setup:
44
+ prerequisites:
45
+ list: []
46
+ configuration:
47
+ file:
48
+ name: go.d/lvm.conf
49
+ options:
50
+ description: |
51
+ The following options can be defined globally: update_every.
52
+ folding:
53
+ title: Config options
54
+ enabled: true
55
+ list:
56
+ - name: update_every
57
+ description: Data collection frequency.
58
+ default_value: 10
59
+ required: false
60
+ - name: timeout
61
+ description: lvs binary execution timeout.
62
+ default_value: 2
63
+ required: false
64
+ examples:
65
+ folding:
66
+ title: Config
67
+ enabled: true
68
+ list:
69
+ - name: Custom update_every
70
+ description: Allows you to override the default data collection interval.
71
+ config: |
72
+ jobs:
73
+ - name: lvm
74
+ update_every: 5 # Collect logical volume statistics every 5 seconds
75
+ troubleshooting:
76
+ problems:
77
+ list: []
78
+ alerts: []
79
+ metrics:
80
+ folding:
81
+ title: Metrics
82
+ enabled: false
83
+ description: ""
84
+ availability: []
85
+ scopes:
86
+ - name: logical volume
87
+ description: These metrics refer to the LVM logical volume.
88
+ labels:
89
+ - name: lv_name
90
+ description: Logical volume name
91
+ - name: vg_name
92
+ description: Volume group name
93
+ - name: volume_type
94
+ description: Type of the volume
95
+ metrics:
96
+ - name: lvm.lv_data_space_utilization
97
+ description: Logical volume space allocated for data
98
+ unit: '%'
99
+ chart_type: area
100
+ dimensions:
101
+ - name: utilization
102
+ - name: lvm.lv_metadata_space_utilization
103
+ description: Logical volume space allocated for metadata
104
+ unit: '%'
105
+ chart_type: area
106
+ dimensions:
107
+ - name: utilization
src/go/collectors/go.d.plugin/modules/lvm/testdata/config.json
new
+4
@@ -0,0 +1,4 @@
1
+{
2
+ "update_every": 123,
3
+ "timeout": 123.123
4
+}
src/go/collectors/go.d.plugin/modules/lvm/testdata/config.yaml
new
+2
@@ -0,0 +1,2 @@
1
+update_every: 123
2
+timeout: 123.123
src/go/collectors/go.d.plugin/modules/lvm/testdata/lvs-report-no-thin.json
new
+16
@@ -0,0 +1,16 @@
1
+{
2
+ "report": [
3
+ {
4
+ "lv": [
5
+ {
6
+ "vg_name": "cm-vg",
7
+ "lv_name": "root",
8
+ "lv_size": "214232465408",
9
+ "data_percent": "",
10
+ "metadata_percent": "",
11
+ "lv_attr": "-wi-ao----"
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+}
src/go/collectors/go.d.plugin/modules/lvm/testdata/lvs-report.json
new
+16
@@ -0,0 +1,16 @@
1
+{
2
+ "report": [
3
+ {
4
+ "lv": [
5
+ {
6
+ "vg_name": "cm-vg",
7
+ "lv_name": "root",
8
+ "lv_size": "214232465408",
9
+ "data_percent": "78.89",
10
+ "metadata_percent": "19.25",
11
+ "lv_attr": "twi-ao----"
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+}
src/go/collectors/go.d.plugin/modules/nvme/testdata/config.json
+1
-2
@@ -1,5 +1,4 @@
1
{
2
"update_every": 123,
3
- "timeout": 123.123,
4
- "binary_path": "ok"
3
+ "timeout": 123.123
4
}
src/go/collectors/go.d.plugin/modules/nvme/testdata/config.yaml
-1
@@ -1,3 +1,2 @@
1
update_every: 123
2
timeout: 123.123
3
-binary_path: "ok"