go.d add dmcache collector (#17947)
Ilya Mashchenko committed
Jun 18, 2024 at 20:07 UTC
bcaa5e7370df3ba5b7eaa5aba511320752726c50
14 files changed
+925
src/go/collectors/go.d.plugin/README.md
+1
@@ -61,6 +61,7 @@ see the appropriate collector readme.
61
| [coredns](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/coredns) | CoreDNS |
62
| [couchbase](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/couchbase) | Couchbase |
63
| [couchdb](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/couchdb) | CouchDB |
64
+| [dmcache](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/dmcache) | DMCache |
65
| [dnsdist](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/dnsdist) | Dnsdist |
66
| [dnsmasq](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/dnsmasq) | Dnsmasq DNS Forwarder |
67
| [dnsmasq_dhcp](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/dnsmasq_dhcp) | Dnsmasq DHCP |
src/go/collectors/go.d.plugin/config/go.d.conf
+1
@@ -26,6 +26,7 @@ modules:
26
# coredns: yes
27
# couchbase: yes
28
# couchdb: yes
29
+# dmcache: yes
30
# dnsdist: yes
31
# dnsmasq: yes
32
# dnsmasq_dhcp: yes
src/go/collectors/go.d.plugin/config/go.d/dmcache.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/dmcache#readme
3
+
4
+jobs:
5
+ - name: dmcache
src/go/collectors/go.d.plugin/modules/dmcache/charts.go
new
+149
@@ -0,0 +1,149 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dmcache
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
10
+)
11
+
12
+const (
13
+ prioDeviceCacheSpaceUsage = module.Priority + iota
14
+ prioDeviceMetaSpaceUsage
15
+ prioDeviceReadEfficiency
16
+ prioDeviceWriteEfficiency
17
+ prioDeviceActivity
18
+ prioDeviceDirty
19
+)
20
+
21
+var deviceChartsTmpl = module.Charts{
22
+ chartDeviceCacheSpaceUsageTmpl.Copy(),
23
+ chartDeviceMetadataSpaceUsageTmpl.Copy(),
24
+
25
+ chartDeviceReadEfficiencyTmpl.Copy(),
26
+ chartDeviceWriteEfficiencyTmpl.Copy(),
27
+
28
+ chartDeviceActivityTmpl.Copy(),
29
+
30
+ chartDeviceDirtySizeTmpl.Copy(),
31
+}
32
+
33
+var (
34
+ chartDeviceCacheSpaceUsageTmpl = module.Chart{
35
+ ID: "dmcache_device_%s_cache_space_usage",
36
+ Title: "DMCache space usage",
37
+ Units: "bytes",
38
+ Fam: "space usage",
39
+ Ctx: "dmcache.device_cache_space_usage",
40
+ Type: module.Stacked,
41
+ Priority: prioDeviceCacheSpaceUsage,
42
+ Dims: module.Dims{
43
+ {ID: "dmcache_device_%s_cache_free_bytes", Name: "free"},
44
+ {ID: "dmcache_device_%s_cache_used_bytes", Name: "used"},
45
+ },
46
+ }
47
+ chartDeviceMetadataSpaceUsageTmpl = module.Chart{
48
+ ID: "dmcache_device_%s_metadata_space_usage",
49
+ Title: "DMCache metadata space usage",
50
+ Units: "bytes",
51
+ Fam: "space usage",
52
+ Ctx: "dmcache.device_metadata_space_usage",
53
+ Type: module.Stacked,
54
+ Priority: prioDeviceMetaSpaceUsage,
55
+ Dims: module.Dims{
56
+ {ID: "dmcache_device_%s_metadata_free_bytes", Name: "free"},
57
+ {ID: "dmcache_device_%s_metadata_used_bytes", Name: "used"},
58
+ },
59
+ }
60
+)
61
+
62
+var (
63
+ chartDeviceReadEfficiencyTmpl = module.Chart{
64
+ ID: "dmcache_device_%s_read_efficiency",
65
+ Title: "DMCache read efficiency",
66
+ Units: "requests/s",
67
+ Fam: "efficiency",
68
+ Ctx: "dmcache.device_cache_read_efficiency",
69
+ Type: module.Stacked,
70
+ Priority: prioDeviceReadEfficiency,
71
+ Dims: module.Dims{
72
+ {ID: "dmcache_device_%s_read_hits", Name: "hits", Algo: module.Incremental},
73
+ {ID: "dmcache_device_%s_read_misses", Name: "misses", Algo: module.Incremental},
74
+ },
75
+ }
76
+ chartDeviceWriteEfficiencyTmpl = module.Chart{
77
+ ID: "dmcache_device_%s_write_efficiency",
78
+ Title: "DMCache write efficiency",
79
+ Units: "requests/s",
80
+ Fam: "efficiency",
81
+ Ctx: "dmcache.device_cache_write_efficiency",
82
+ Type: module.Stacked,
83
+ Priority: prioDeviceWriteEfficiency,
84
+ Dims: module.Dims{
85
+ {ID: "dmcache_device_%s_write_hits", Name: "hits", Algo: module.Incremental},
86
+ {ID: "dmcache_device_%s_write_misses", Name: "misses", Algo: module.Incremental},
87
+ },
88
+ }
89
+)
90
+
91
+var chartDeviceActivityTmpl = module.Chart{
92
+ ID: "dmcache_device_%s_activity",
93
+ Title: "DMCache activity",
94
+ Units: "bytes/s",
95
+ Fam: "activity",
96
+ Ctx: "dmcache.device_cache_activity",
97
+ Type: module.Area,
98
+ Priority: prioDeviceActivity,
99
+ Dims: module.Dims{
100
+ {ID: "dmcache_device_%s_promotions_bytes", Name: "promotions", Algo: module.Incremental},
101
+ {ID: "dmcache_device_%s_demotions_bytes", Name: "demotions", Mul: -1, Algo: module.Incremental},
102
+ },
103
+}
104
+
105
+var chartDeviceDirtySizeTmpl = module.Chart{
106
+ ID: "dmcache_device_%s_dirty_size",
107
+ Title: "DMCache dirty data size",
108
+ Units: "bytes",
109
+ Fam: "dirty size",
110
+ Ctx: "dmcache.device_cache_dirty_size",
111
+ Type: module.Area,
112
+ Priority: prioDeviceDirty,
113
+ Dims: module.Dims{
114
+ {ID: "dmcache_device_%s_dirty_bytes", Name: "dirty"},
115
+ },
116
+}
117
+
118
+func (c *DmCache) addDeviceCharts(device string) {
119
+ charts := deviceChartsTmpl.Copy()
120
+
121
+ for _, chart := range *charts {
122
+ chart.ID = fmt.Sprintf(chart.ID, cleanDeviceName(device))
123
+ chart.Labels = []module.Label{
124
+ {Key: "device", Value: device},
125
+ }
126
+ for _, dim := range chart.Dims {
127
+ dim.ID = fmt.Sprintf(dim.ID, device)
128
+ }
129
+ }
130
+
131
+ if err := c.Charts().Add(*charts...); err != nil {
132
+ c.Warning(err)
133
+ }
134
+}
135
+
136
+func (c *DmCache) removeDeviceCharts(device string) {
137
+ px := fmt.Sprintf("dmcache_device_%s_", cleanDeviceName(device))
138
+
139
+ for _, chart := range *c.Charts() {
140
+ if strings.HasPrefix(chart.ID, px) {
141
+ chart.MarkRemove()
142
+ chart.MarkNotCreated()
143
+ }
144
+ }
145
+}
146
+
147
+func cleanDeviceName(device string) string {
148
+ return strings.ReplaceAll(device, ".", "_")
149
+}
src/go/collectors/go.d.plugin/modules/dmcache/collect.go
new
+173
@@ -0,0 +1,173 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dmcache
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+)
13
+
14
+type dmCacheDevice struct {
15
+ name string
16
+ metaBlockSizeSectors int64
17
+ metaUsedBlocks int64
18
+ metaTotalBlocks int64
19
+ cacheBlockSizeSectors int64
20
+ cacheUsedBlocks int64
21
+ cacheTotalBlocks int64
22
+ readHits int64
23
+ readMisses int64
24
+ writeHits int64
25
+ writeMisses int64
26
+ demotionsBlocks int64
27
+ promotionsBlocks int64
28
+ dirtyBlocks int64
29
+}
30
+
31
+func (c *DmCache) collect() (map[string]int64, error) {
32
+ bs, err := c.exec.cacheStatus()
33
+ if err != nil {
34
+ return nil, err
35
+ }
36
+
37
+ mx := make(map[string]int64)
38
+
39
+ if err := c.collectCacheStatus(mx, bs); err != nil {
40
+ return nil, err
41
+ }
42
+
43
+ return mx, nil
44
+}
45
+
46
+func (c *DmCache) collectCacheStatus(mx map[string]int64, data []byte) error {
47
+ var devices []*dmCacheDevice
48
+
49
+ sc := bufio.NewScanner(bytes.NewReader(data))
50
+
51
+ for sc.Scan() {
52
+ line := strings.TrimSpace(sc.Text())
53
+ if line == "" {
54
+ continue
55
+ }
56
+
57
+ dev, err := parseDmsetupStatusLine(line)
58
+ if err != nil {
59
+ return fmt.Errorf("malformed dmsetup status line: %v ('%s')", err, line)
60
+ }
61
+
62
+ devices = append(devices, dev)
63
+ }
64
+
65
+ seen := make(map[string]bool)
66
+
67
+ for _, dev := range devices {
68
+ seen[dev.name] = true
69
+
70
+ if !c.devices[dev.name] {
71
+ c.devices[dev.name] = true
72
+ c.addDeviceCharts(dev.name)
73
+ }
74
+
75
+ px := fmt.Sprintf("dmcache_device_%s_", dev.name)
76
+
77
+ const sectorSize = 512
78
+ metaMul := dev.metaBlockSizeSectors * sectorSize
79
+ cacheMul := dev.cacheBlockSizeSectors * sectorSize
80
+
81
+ mx[px+"metadata_free_bytes"] = (dev.metaTotalBlocks - dev.metaUsedBlocks) * metaMul
82
+ mx[px+"metadata_used_bytes"] = dev.metaUsedBlocks * metaMul
83
+ mx[px+"cache_free_bytes"] = (dev.cacheTotalBlocks - dev.cacheUsedBlocks) * cacheMul
84
+ mx[px+"cache_used_bytes"] = dev.cacheUsedBlocks * cacheMul
85
+ mx[px+"read_hits"] = dev.readHits
86
+ mx[px+"read_misses"] = dev.readMisses
87
+ mx[px+"write_hits"] = dev.writeHits
88
+ mx[px+"write_misses"] = dev.writeMisses
89
+ mx[px+"demotions_bytes"] = dev.demotionsBlocks * cacheMul
90
+ mx[px+"promotions_bytes"] = dev.promotionsBlocks * cacheMul
91
+ mx[px+"dirty_bytes"] = dev.dirtyBlocks * cacheMul
92
+ }
93
+
94
+ for dev := range c.devices {
95
+ if !seen[dev] {
96
+ delete(c.devices, dev)
97
+ c.removeDeviceCharts(dev)
98
+ }
99
+ }
100
+
101
+ if len(devices) == 0 {
102
+ return errors.New("no dm-cache devices found")
103
+ }
104
+
105
+ return nil
106
+}
107
+
108
+func parseDmsetupStatusLine(line string) (*dmCacheDevice, error) {
109
+ // https://www.kernel.org/doc/html/next/admin-guide/device-mapper/cache.html#status
110
+
111
+ parts := strings.Fields(line)
112
+ if len(parts) < 15 {
113
+ return nil, fmt.Errorf("want at least 15 fields, got %d", len(parts))
114
+ }
115
+
116
+ var dev dmCacheDevice
117
+ var err error
118
+
119
+ for i, s := range parts {
120
+ switch i {
121
+ case 0:
122
+ dev.name = strings.TrimSuffix(parts[0], ":")
123
+ case 4:
124
+ dev.metaBlockSizeSectors, err = parseInt(s)
125
+ case 5:
126
+ dev.metaUsedBlocks, dev.metaTotalBlocks, err = parseUsedTotalBlocks(s)
127
+ case 6:
128
+ dev.cacheBlockSizeSectors, err = parseInt(s)
129
+ case 7:
130
+ dev.cacheUsedBlocks, dev.cacheTotalBlocks, err = parseUsedTotalBlocks(s)
131
+ case 8:
132
+ dev.readHits, err = parseInt(s)
133
+ case 9:
134
+ dev.readMisses, err = parseInt(s)
135
+ case 10:
136
+ dev.writeHits, err = parseInt(s)
137
+ case 11:
138
+ dev.writeMisses, err = parseInt(s)
139
+ case 12:
140
+ dev.demotionsBlocks, err = parseInt(s)
141
+ case 13:
142
+ dev.promotionsBlocks, err = parseInt(s)
143
+ case 14:
144
+ dev.dirtyBlocks, err = parseInt(s)
145
+ }
146
+
147
+ if err != nil {
148
+ return nil, fmt.Errorf("failed to parse %d field '%s': %v", i, s, err)
149
+ }
150
+ }
151
+
152
+ return &dev, nil
153
+}
154
+
155
+func parseUsedTotalBlocks(info string) (int64, int64, error) {
156
+ parts := strings.Split(info, "/")
157
+ if len(parts) != 2 {
158
+ return 0, 0, errors.New("expected used/total")
159
+ }
160
+ used, err := parseInt(parts[0])
161
+ if err != nil {
162
+ return 0, 0, err
163
+ }
164
+ total, err := parseInt(parts[1])
165
+ if err != nil {
166
+ return 0, 0, err
167
+ }
168
+ return used, total, nil
169
+}
170
+
171
+func parseInt(s string) (int64, error) {
172
+ return strconv.ParseInt(s, 10, 64)
173
+}
src/go/collectors/go.d.plugin/modules/dmcache/config_schema.json
new
+35
@@ -0,0 +1,35 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "DMCache 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/dmcache/dmcache.go
new
+105
@@ -0,0 +1,105 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dmcache
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("dmcache", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Defaults: module.Defaults{
21
+ UpdateEvery: 10,
22
+ },
23
+ Create: func() module.Module { return New() },
24
+ Config: func() any { return &Config{} },
25
+ })
26
+}
27
+
28
+func New() *DmCache {
29
+ return &DmCache{
30
+ Config: Config{
31
+ Timeout: web.Duration(time.Second * 2),
32
+ },
33
+ charts: &module.Charts{},
34
+ devices: make(map[string]bool),
35
+ }
36
+}
37
+
38
+type Config struct {
39
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
40
+ Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
41
+}
42
+
43
+type (
44
+ DmCache struct {
45
+ module.Base
46
+ Config `yaml:",inline" json:""`
47
+
48
+ charts *module.Charts
49
+
50
+ exec dmsetupCLI
51
+
52
+ devices map[string]bool
53
+ }
54
+ dmsetupCLI interface {
55
+ cacheStatus() ([]byte, error)
56
+ }
57
+)
58
+
59
+func (c *DmCache) Configuration() any {
60
+ return c.Config
61
+}
62
+
63
+func (c *DmCache) Init() error {
64
+ dmsetup, err := c.initDmsetupCLI()
65
+ if err != nil {
66
+ c.Errorf("dmsetup exec initialization: %v", err)
67
+ return err
68
+ }
69
+ c.exec = dmsetup
70
+
71
+ return nil
72
+}
73
+
74
+func (c *DmCache) Check() error {
75
+ mx, err := c.collect()
76
+ if err != nil {
77
+ c.Error(err)
78
+ return err
79
+ }
80
+
81
+ if len(mx) == 0 {
82
+ return errors.New("no metrics collected")
83
+ }
84
+
85
+ return nil
86
+}
87
+
88
+func (c *DmCache) Charts() *module.Charts {
89
+ return c.charts
90
+}
91
+
92
+func (c *DmCache) Collect() map[string]int64 {
93
+ mx, err := c.collect()
94
+ if err != nil {
95
+ c.Error(err)
96
+ }
97
+
98
+ if len(mx) == 0 {
99
+ return nil
100
+ }
101
+
102
+ return mx
103
+}
104
+
105
+func (c *DmCache) Cleanup() {}
src/go/collectors/go.d.plugin/modules/dmcache/dmcache_test.go
new
+253
@@ -0,0 +1,253 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dmcache
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
+
21
+func Test_testDataIsValid(t *testing.T) {
22
+ for name, data := range map[string][]byte{
23
+ "dataConfigJSON": dataConfigJSON,
24
+ "dataConfigYAML": dataConfigYAML,
25
+ } {
26
+ require.NotNil(t, data, name)
27
+
28
+ }
29
+}
30
+
31
+func TestDmCace_Configuration(t *testing.T) {
32
+ module.TestConfigurationSerialize(t, &DmCache{}, dataConfigJSON, dataConfigYAML)
33
+}
34
+
35
+func TestDmCache_Init(t *testing.T) {
36
+ tests := map[string]struct {
37
+ config Config
38
+ wantFail bool
39
+ }{
40
+ "fails if failed to locate ndsudo": {
41
+ wantFail: true,
42
+ config: New().Config,
43
+ },
44
+ }
45
+
46
+ for name, test := range tests {
47
+ t.Run(name, func(t *testing.T) {
48
+ lvm := New()
49
+ lvm.Config = test.config
50
+
51
+ if test.wantFail {
52
+ assert.Error(t, lvm.Init())
53
+ } else {
54
+ assert.NoError(t, lvm.Init())
55
+ }
56
+ })
57
+ }
58
+}
59
+
60
+func TestDmCache_Cleanup(t *testing.T) {
61
+ tests := map[string]struct {
62
+ prepare func() *DmCache
63
+ }{
64
+ "not initialized exec": {
65
+ prepare: func() *DmCache {
66
+ return New()
67
+ },
68
+ },
69
+ "after check": {
70
+ prepare: func() *DmCache {
71
+ lvm := New()
72
+ lvm.exec = prepareMockOK()
73
+ _ = lvm.Check()
74
+ return lvm
75
+ },
76
+ },
77
+ "after collect": {
78
+ prepare: func() *DmCache {
79
+ lvm := New()
80
+ lvm.exec = prepareMockOK()
81
+ _ = lvm.Collect()
82
+ return lvm
83
+ },
84
+ },
85
+ }
86
+
87
+ for name, test := range tests {
88
+ t.Run(name, func(t *testing.T) {
89
+ lvm := test.prepare()
90
+
91
+ assert.NotPanics(t, lvm.Cleanup)
92
+ })
93
+ }
94
+}
95
+
96
+func TestDmCache_Charts(t *testing.T) {
97
+ assert.NotNil(t, New().Charts())
98
+}
99
+
100
+func TestDmCache_Check(t *testing.T) {
101
+ tests := map[string]struct {
102
+ prepareMock func() *mockDmsetupExec
103
+ wantFail bool
104
+ }{
105
+ "success case": {
106
+ prepareMock: prepareMockOK,
107
+ wantFail: false,
108
+ },
109
+ "error on cache status": {
110
+ prepareMock: prepareMockErr,
111
+ wantFail: true,
112
+ },
113
+ "empty response": {
114
+ prepareMock: prepareMockEmptyResponse,
115
+ wantFail: true,
116
+ },
117
+ "unexpected response": {
118
+ prepareMock: prepareMockUnexpectedResponse,
119
+ wantFail: true,
120
+ },
121
+ }
122
+
123
+ for name, test := range tests {
124
+ t.Run(name, func(t *testing.T) {
125
+ dmcache := New()
126
+ mock := test.prepareMock()
127
+ dmcache.exec = mock
128
+
129
+ if test.wantFail {
130
+ assert.Error(t, dmcache.Check())
131
+ } else {
132
+ assert.NoError(t, dmcache.Check())
133
+ }
134
+ })
135
+ }
136
+}
137
+
138
+func TestLVM_Collect(t *testing.T) {
139
+ tests := map[string]struct {
140
+ prepareMock func() *mockDmsetupExec
141
+ wantCharts int
142
+ wantMetrics map[string]int64
143
+ }{
144
+ "success case": {
145
+ prepareMock: prepareMockOK,
146
+ wantCharts: len(deviceChartsTmpl) * 2,
147
+ wantMetrics: map[string]int64{
148
+ "dmcache_device_vg_raid1_md21-media_cache_free_bytes": 1252402397184,
149
+ "dmcache_device_vg_raid1_md21-media_cache_used_bytes": 396412059648,
150
+ "dmcache_device_vg_raid1_md21-media_demotions_bytes": 0,
151
+ "dmcache_device_vg_raid1_md21-media_dirty_bytes": 0,
152
+ "dmcache_device_vg_raid1_md21-media_metadata_free_bytes": 32243712,
153
+ "dmcache_device_vg_raid1_md21-media_metadata_used_bytes": 9699328,
154
+ "dmcache_device_vg_raid1_md21-media_promotions_bytes": 48035266560,
155
+ "dmcache_device_vg_raid1_md21-media_read_hits": 82870357,
156
+ "dmcache_device_vg_raid1_md21-media_read_misses": 5499462,
157
+ "dmcache_device_vg_raid1_md21-media_write_hits": 26280342,
158
+ "dmcache_device_vg_raid1_md21-media_write_misses": 8017854,
159
+ "dmcache_device_vg_raid2_md22-media_cache_free_bytes": 1252402397184,
160
+ "dmcache_device_vg_raid2_md22-media_cache_used_bytes": 396412059648,
161
+ "dmcache_device_vg_raid2_md22-media_demotions_bytes": 0,
162
+ "dmcache_device_vg_raid2_md22-media_dirty_bytes": 0,
163
+ "dmcache_device_vg_raid2_md22-media_metadata_free_bytes": 32243712,
164
+ "dmcache_device_vg_raid2_md22-media_metadata_used_bytes": 9699328,
165
+ "dmcache_device_vg_raid2_md22-media_promotions_bytes": 48035266560,
166
+ "dmcache_device_vg_raid2_md22-media_read_hits": 82870357,
167
+ "dmcache_device_vg_raid2_md22-media_read_misses": 5499462,
168
+ "dmcache_device_vg_raid2_md22-media_write_hits": 26280342,
169
+ "dmcache_device_vg_raid2_md22-media_write_misses": 8017854,
170
+ },
171
+ },
172
+ "error on cache status": {
173
+ prepareMock: prepareMockErr,
174
+ wantMetrics: nil,
175
+ },
176
+ "empty response": {
177
+ prepareMock: prepareMockEmptyResponse,
178
+ wantMetrics: nil,
179
+ },
180
+ "unexpected response": {
181
+ prepareMock: prepareMockUnexpectedResponse,
182
+ wantMetrics: nil,
183
+ },
184
+ }
185
+
186
+ for name, test := range tests {
187
+ t.Run(name, func(t *testing.T) {
188
+ dmcache := New()
189
+ mock := test.prepareMock()
190
+ dmcache.exec = mock
191
+
192
+ mx := dmcache.Collect()
193
+
194
+ assert.Equal(t, test.wantMetrics, mx)
195
+ assert.Len(t, *dmcache.Charts(), test.wantCharts)
196
+ testMetricsHasAllChartsDims(t, dmcache, mx)
197
+ })
198
+ }
199
+}
200
+
201
+func testMetricsHasAllChartsDims(t *testing.T, dmcache *DmCache, mx map[string]int64) {
202
+ for _, chart := range *dmcache.Charts() {
203
+ if chart.Obsolete {
204
+ continue
205
+ }
206
+ for _, dim := range chart.Dims {
207
+ _, ok := mx[dim.ID]
208
+ assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
209
+ }
210
+ }
211
+}
212
+
213
+func prepareMockOK() *mockDmsetupExec {
214
+ return &mockDmsetupExec{
215
+ cacheStatusData: []byte(`
216
+vg_raid1_md21-media: 0 2404139008 cache 8 2368/10240 4096 189024/786216 82870357 5499462 26280342 8017854 0 22905 0 3 metadata2 writethrough no_discard_passdown 2 migration_threshold 32768 mq 10 random_threshold 0 sequential_threshold 0 discard_promote_adjustment 0 read_promote_adjustment 0 write_promote_adjustment 0 rw -
217
+vg_raid2_md22-media: 0 2404139008 cache 8 2368/10240 4096 189024/786216 82870357 5499462 26280342 8017854 0 22905 0 3 metadata2 writethrough no_discard_passdown 2 migration_threshold 32768 mq 10 random_threshold 0 sequential_threshold 0 discard_promote_adjustment 0 read_promote_adjustment 0 write_promote_adjustment 0 rw -
218
+`),
219
+ }
220
+}
221
+
222
+func prepareMockErr() *mockDmsetupExec {
223
+ return &mockDmsetupExec{
224
+ errOnCacheStatus: true,
225
+ }
226
+}
227
+
228
+func prepareMockEmptyResponse() *mockDmsetupExec {
229
+ return &mockDmsetupExec{}
230
+}
231
+
232
+func prepareMockUnexpectedResponse() *mockDmsetupExec {
233
+ return &mockDmsetupExec{
234
+ cacheStatusData: []byte(`
235
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
236
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
237
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
238
+`),
239
+ }
240
+}
241
+
242
+type mockDmsetupExec struct {
243
+ errOnCacheStatus bool
244
+ cacheStatusData []byte
245
+}
246
+
247
+func (m *mockDmsetupExec) cacheStatus() ([]byte, error) {
248
+ if m.errOnCacheStatus {
249
+ return nil, errors.New("mock.cacheStatus() error")
250
+ }
251
+
252
+ return m.cacheStatusData, nil
253
+}
src/go/collectors/go.d.plugin/modules/dmcache/exec.go
new
+42
@@ -0,0 +1,42 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dmcache
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 newDmsetupExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *dmsetupExec {
15
+ return &dmsetupExec{
16
+ Logger: log,
17
+ ndsudoPath: ndsudoPath,
18
+ timeout: timeout,
19
+ }
20
+}
21
+
22
+type dmsetupExec struct {
23
+ *logger.Logger
24
+
25
+ ndsudoPath string
26
+ timeout time.Duration
27
+}
28
+
29
+func (e *dmsetupExec) cacheStatus() ([]byte, error) {
30
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
31
+ defer cancel()
32
+
33
+ cmd := exec.CommandContext(ctx, e.ndsudoPath, "dmsetup-status-cache")
34
+ e.Debugf("executing '%s'", cmd)
35
+
36
+ bs, err := cmd.Output()
37
+ if err != nil {
38
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
39
+ }
40
+
41
+ return bs, nil
42
+}
src/go/collectors/go.d.plugin/modules/dmcache/init.go
new
+23
@@ -0,0 +1,23 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dmcache
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 (c *DmCache) initDmsetupCLI() (dmsetupCLI, 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
+ dmsetup := newDmsetupExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
21
+
22
+ return dmsetup, nil
23
+}
src/go/collectors/go.d.plugin/modules/dmcache/metadata.yaml
new
+131
@@ -0,0 +1,131 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-dmcache
5
+ plugin_name: go.d.plugin
6
+ module_name: dmcache
7
+ monitored_instance:
8
+ name: DMCache devices
9
+ link: ""
10
+ icon_filename: filesystem.svg
11
+ categories:
12
+ - data-collection.storage-mount-points-and-filesystems
13
+ keywords:
14
+ - dmcache
15
+ related_resources:
16
+ integrations:
17
+ list: []
18
+ info_provided_to_referring_integrations:
19
+ description: ""
20
+ most_popular: false
21
+ overview:
22
+ data_collection:
23
+ metrics_description: >
24
+ This collector monitors DMCache, providing insights into capacity usage, efficiency, and activity.
25
+ It relies on the [`dmsetup`](https://man7.org/linux/man-pages/man8/dmsetup.8.html) CLI tool but avoids directly executing the binary.
26
+ Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment.
27
+ This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management.
28
+ method_description: ""
29
+ supported_platforms:
30
+ include: []
31
+ exclude: []
32
+ multi_instance: false
33
+ additional_permissions:
34
+ description: ""
35
+ default_behavior:
36
+ auto_detection:
37
+ description: ""
38
+ limits:
39
+ description: ""
40
+ performance_impact:
41
+ description: ""
42
+ setup:
43
+ prerequisites:
44
+ list: []
45
+ configuration:
46
+ file:
47
+ name: go.d/dmcache.conf
48
+ options:
49
+ description: |
50
+ The following options can be defined globally: update_every.
51
+ folding:
52
+ title: Config options
53
+ enabled: true
54
+ list:
55
+ - name: update_every
56
+ description: Data collection frequency.
57
+ default_value: 10
58
+ required: false
59
+ - name: timeout
60
+ description: dmsetup binary execution timeout.
61
+ default_value: 2
62
+ required: false
63
+ examples:
64
+ folding:
65
+ title: Config
66
+ enabled: true
67
+ list:
68
+ - name: Custom update_every
69
+ description: Allows you to override the default data collection interval.
70
+ config: |
71
+ jobs:
72
+ - name: dmcache
73
+ update_every: 5 # Collect DMCache statistics every 5 seconds
74
+ troubleshooting:
75
+ problems:
76
+ list: []
77
+ alerts: []
78
+ metrics:
79
+ folding:
80
+ title: Metrics
81
+ enabled: false
82
+ description: ""
83
+ availability: []
84
+ scopes:
85
+ - name: dmcache device
86
+ description: These metrics refer to the DMCache device.
87
+ labels:
88
+ - name: device
89
+ description: Device name
90
+ metrics:
91
+ - name: dmcache.device_cache_space_usage
92
+ description: DMCache space usage
93
+ unit: bytes
94
+ chart_type: stacked
95
+ dimensions:
96
+ - name: free
97
+ - name: used
98
+ - name: dmcache.device_metadata_space_usage
99
+ description: DMCache metadata space usage
100
+ unit: bytes
101
+ chart_type: stacked
102
+ dimensions:
103
+ - name: free
104
+ - name: used
105
+ - name: dmcache.device_cache_read_efficiency
106
+ description: DMCache read efficiency
107
+ unit: requests/s
108
+ chart_type: stacked
109
+ dimensions:
110
+ - name: hits
111
+ - name: misses
112
+ - name: dmcache.device_cache_write_efficiency
113
+ description: DMCache write efficiency
114
+ unit: requests/s
115
+ chart_type: stacked
116
+ dimensions:
117
+ - name: hits
118
+ - name: misses
119
+ - name: dmcache.device_cache_activity
120
+ description: DMCache activity
121
+ unit: bytes/s
122
+ chart_type: area
123
+ dimensions:
124
+ - name: promotions
125
+ - name: demotions
126
+ - name: dmcache.device_cache_dirty_size
127
+ description: DMCache dirty data size
128
+ unit: bytes
129
+ chart_type: area
130
+ dimensions:
131
+ - name: dirty
src/go/collectors/go.d.plugin/modules/dmcache/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/dmcache/testdata/config.yaml
new
+2
@@ -0,0 +1,2 @@
1
+update_every: 123
2
+timeout: 123.123
src/go/collectors/go.d.plugin/modules/init.go
+1
@@ -15,6 +15,7 @@ import (
15
_ "github.com/netdata/netdata/go/go.d.plugin/modules/coredns"
16
_ "github.com/netdata/netdata/go/go.d.plugin/modules/couchbase"
17
_ "github.com/netdata/netdata/go/go.d.plugin/modules/couchdb"
18
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/dmcache"
19
_ "github.com/netdata/netdata/go/go.d.plugin/modules/dnsdist"
20
_ "github.com/netdata/netdata/go/go.d.plugin/modules/dnsmasq"
21
_ "github.com/netdata/netdata/go/go.d.plugin/modules/dnsmasq_dhcp"