@cryptotaxi247 / netdata-1 / commits / bcaa5e737

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"