diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 08:15:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 08:15:20 +0000 |
commit | 87d772a7d708fec12f48cd8adc0dedff6e1025da (patch) | |
tree | 1fee344c64cc3f43074a01981e21126c8482a522 /src/go/plugin/go.d/modules/dmcache | |
parent | Adding upstream version 1.46.3. (diff) | |
download | netdata-87d772a7d708fec12f48cd8adc0dedff6e1025da.tar.xz netdata-87d772a7d708fec12f48cd8adc0dedff6e1025da.zip |
Adding upstream version 1.47.0.upstream/1.47.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/go/plugin/go.d/modules/dmcache')
l--------- | src/go/plugin/go.d/modules/dmcache/README.md | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/charts.go | 149 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/collect.go | 173 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/config_schema.json | 35 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/dmcache.go | 105 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/dmcache_test.go | 253 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/exec.go | 42 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/init.go | 23 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/integrations/dmcache_devices.md | 198 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/metadata.yaml | 131 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/testdata/config.json | 4 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/dmcache/testdata/config.yaml | 2 |
12 files changed, 1116 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/dmcache/README.md b/src/go/plugin/go.d/modules/dmcache/README.md new file mode 120000 index 000000000..9609ec869 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/README.md @@ -0,0 +1 @@ +integrations/dmcache_devices.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/dmcache/charts.go b/src/go/plugin/go.d/modules/dmcache/charts.go new file mode 100644 index 000000000..c77f3d878 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/charts.go @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dmcache + +import ( + "fmt" + "strings" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +const ( + prioDeviceCacheSpaceUsage = module.Priority + iota + prioDeviceMetaSpaceUsage + prioDeviceReadEfficiency + prioDeviceWriteEfficiency + prioDeviceActivity + prioDeviceDirty +) + +var deviceChartsTmpl = module.Charts{ + chartDeviceCacheSpaceUsageTmpl.Copy(), + chartDeviceMetadataSpaceUsageTmpl.Copy(), + + chartDeviceReadEfficiencyTmpl.Copy(), + chartDeviceWriteEfficiencyTmpl.Copy(), + + chartDeviceActivityTmpl.Copy(), + + chartDeviceDirtySizeTmpl.Copy(), +} + +var ( + chartDeviceCacheSpaceUsageTmpl = module.Chart{ + ID: "dmcache_device_%s_cache_space_usage", + Title: "DMCache space usage", + Units: "bytes", + Fam: "space usage", + Ctx: "dmcache.device_cache_space_usage", + Type: module.Stacked, + Priority: prioDeviceCacheSpaceUsage, + Dims: module.Dims{ + {ID: "dmcache_device_%s_cache_free_bytes", Name: "free"}, + {ID: "dmcache_device_%s_cache_used_bytes", Name: "used"}, + }, + } + chartDeviceMetadataSpaceUsageTmpl = module.Chart{ + ID: "dmcache_device_%s_metadata_space_usage", + Title: "DMCache metadata space usage", + Units: "bytes", + Fam: "space usage", + Ctx: "dmcache.device_metadata_space_usage", + Type: module.Stacked, + Priority: prioDeviceMetaSpaceUsage, + Dims: module.Dims{ + {ID: "dmcache_device_%s_metadata_free_bytes", Name: "free"}, + {ID: "dmcache_device_%s_metadata_used_bytes", Name: "used"}, + }, + } +) + +var ( + chartDeviceReadEfficiencyTmpl = module.Chart{ + ID: "dmcache_device_%s_read_efficiency", + Title: "DMCache read efficiency", + Units: "requests/s", + Fam: "efficiency", + Ctx: "dmcache.device_cache_read_efficiency", + Type: module.Stacked, + Priority: prioDeviceReadEfficiency, + Dims: module.Dims{ + {ID: "dmcache_device_%s_read_hits", Name: "hits", Algo: module.Incremental}, + {ID: "dmcache_device_%s_read_misses", Name: "misses", Algo: module.Incremental}, + }, + } + chartDeviceWriteEfficiencyTmpl = module.Chart{ + ID: "dmcache_device_%s_write_efficiency", + Title: "DMCache write efficiency", + Units: "requests/s", + Fam: "efficiency", + Ctx: "dmcache.device_cache_write_efficiency", + Type: module.Stacked, + Priority: prioDeviceWriteEfficiency, + Dims: module.Dims{ + {ID: "dmcache_device_%s_write_hits", Name: "hits", Algo: module.Incremental}, + {ID: "dmcache_device_%s_write_misses", Name: "misses", Algo: module.Incremental}, + }, + } +) + +var chartDeviceActivityTmpl = module.Chart{ + ID: "dmcache_device_%s_activity", + Title: "DMCache activity", + Units: "bytes/s", + Fam: "activity", + Ctx: "dmcache.device_cache_activity", + Type: module.Area, + Priority: prioDeviceActivity, + Dims: module.Dims{ + {ID: "dmcache_device_%s_promotions_bytes", Name: "promotions", Algo: module.Incremental}, + {ID: "dmcache_device_%s_demotions_bytes", Name: "demotions", Mul: -1, Algo: module.Incremental}, + }, +} + +var chartDeviceDirtySizeTmpl = module.Chart{ + ID: "dmcache_device_%s_dirty_size", + Title: "DMCache dirty data size", + Units: "bytes", + Fam: "dirty size", + Ctx: "dmcache.device_cache_dirty_size", + Type: module.Area, + Priority: prioDeviceDirty, + Dims: module.Dims{ + {ID: "dmcache_device_%s_dirty_bytes", Name: "dirty"}, + }, +} + +func (c *DmCache) addDeviceCharts(device string) { + charts := deviceChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, cleanDeviceName(device)) + chart.Labels = []module.Label{ + {Key: "device", Value: device}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, device) + } + } + + if err := c.Charts().Add(*charts...); err != nil { + c.Warning(err) + } +} + +func (c *DmCache) removeDeviceCharts(device string) { + px := fmt.Sprintf("dmcache_device_%s_", cleanDeviceName(device)) + + for _, chart := range *c.Charts() { + if strings.HasPrefix(chart.ID, px) { + chart.MarkRemove() + chart.MarkNotCreated() + } + } +} + +func cleanDeviceName(device string) string { + return strings.ReplaceAll(device, ".", "_") +} diff --git a/src/go/plugin/go.d/modules/dmcache/collect.go b/src/go/plugin/go.d/modules/dmcache/collect.go new file mode 100644 index 000000000..eae961b73 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/collect.go @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dmcache + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "strconv" + "strings" +) + +type dmCacheDevice struct { + name string + metaBlockSizeSectors int64 + metaUsedBlocks int64 + metaTotalBlocks int64 + cacheBlockSizeSectors int64 + cacheUsedBlocks int64 + cacheTotalBlocks int64 + readHits int64 + readMisses int64 + writeHits int64 + writeMisses int64 + demotionsBlocks int64 + promotionsBlocks int64 + dirtyBlocks int64 +} + +func (c *DmCache) collect() (map[string]int64, error) { + bs, err := c.exec.cacheStatus() + if err != nil { + return nil, err + } + + mx := make(map[string]int64) + + if err := c.collectCacheStatus(mx, bs); err != nil { + return nil, err + } + + return mx, nil +} + +func (c *DmCache) collectCacheStatus(mx map[string]int64, data []byte) error { + var devices []*dmCacheDevice + + sc := bufio.NewScanner(bytes.NewReader(data)) + + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" { + continue + } + + dev, err := parseDmsetupStatusLine(line) + if err != nil { + return fmt.Errorf("malformed dmsetup status line: %v ('%s')", err, line) + } + + devices = append(devices, dev) + } + + seen := make(map[string]bool) + + for _, dev := range devices { + seen[dev.name] = true + + if !c.devices[dev.name] { + c.devices[dev.name] = true + c.addDeviceCharts(dev.name) + } + + px := fmt.Sprintf("dmcache_device_%s_", dev.name) + + const sectorSize = 512 + metaMul := dev.metaBlockSizeSectors * sectorSize + cacheMul := dev.cacheBlockSizeSectors * sectorSize + + mx[px+"metadata_free_bytes"] = (dev.metaTotalBlocks - dev.metaUsedBlocks) * metaMul + mx[px+"metadata_used_bytes"] = dev.metaUsedBlocks * metaMul + mx[px+"cache_free_bytes"] = (dev.cacheTotalBlocks - dev.cacheUsedBlocks) * cacheMul + mx[px+"cache_used_bytes"] = dev.cacheUsedBlocks * cacheMul + mx[px+"read_hits"] = dev.readHits + mx[px+"read_misses"] = dev.readMisses + mx[px+"write_hits"] = dev.writeHits + mx[px+"write_misses"] = dev.writeMisses + mx[px+"demotions_bytes"] = dev.demotionsBlocks * cacheMul + mx[px+"promotions_bytes"] = dev.promotionsBlocks * cacheMul + mx[px+"dirty_bytes"] = dev.dirtyBlocks * cacheMul + } + + for dev := range c.devices { + if !seen[dev] { + delete(c.devices, dev) + c.removeDeviceCharts(dev) + } + } + + if len(devices) == 0 { + return errors.New("no dm-cache devices found") + } + + return nil +} + +func parseDmsetupStatusLine(line string) (*dmCacheDevice, error) { + // https://www.kernel.org/doc/html/next/admin-guide/device-mapper/cache.html#status + + parts := strings.Fields(line) + if len(parts) < 15 { + return nil, fmt.Errorf("want at least 15 fields, got %d", len(parts)) + } + + var dev dmCacheDevice + var err error + + for i, s := range parts { + switch i { + case 0: + dev.name = strings.TrimSuffix(parts[0], ":") + case 4: + dev.metaBlockSizeSectors, err = parseInt(s) + case 5: + dev.metaUsedBlocks, dev.metaTotalBlocks, err = parseUsedTotalBlocks(s) + case 6: + dev.cacheBlockSizeSectors, err = parseInt(s) + case 7: + dev.cacheUsedBlocks, dev.cacheTotalBlocks, err = parseUsedTotalBlocks(s) + case 8: + dev.readHits, err = parseInt(s) + case 9: + dev.readMisses, err = parseInt(s) + case 10: + dev.writeHits, err = parseInt(s) + case 11: + dev.writeMisses, err = parseInt(s) + case 12: + dev.demotionsBlocks, err = parseInt(s) + case 13: + dev.promotionsBlocks, err = parseInt(s) + case 14: + dev.dirtyBlocks, err = parseInt(s) + } + + if err != nil { + return nil, fmt.Errorf("failed to parse %d field '%s': %v", i, s, err) + } + } + + return &dev, nil +} + +func parseUsedTotalBlocks(info string) (int64, int64, error) { + parts := strings.Split(info, "/") + if len(parts) != 2 { + return 0, 0, errors.New("expected used/total") + } + used, err := parseInt(parts[0]) + if err != nil { + return 0, 0, err + } + total, err := parseInt(parts[1]) + if err != nil { + return 0, 0, err + } + return used, total, nil +} + +func parseInt(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} diff --git a/src/go/plugin/go.d/modules/dmcache/config_schema.json b/src/go/plugin/go.d/modules/dmcache/config_schema.json new file mode 100644 index 000000000..4428b4d1b --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/config_schema.json @@ -0,0 +1,35 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DMCache collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 10 + }, + "timeout": { + "title": "Timeout", + "description": "Timeout for executing the binary, specified in seconds.", + "type": "number", + "minimum": 0.5, + "default": 2 + } + }, + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + } + } +} diff --git a/src/go/plugin/go.d/modules/dmcache/dmcache.go b/src/go/plugin/go.d/modules/dmcache/dmcache.go new file mode 100644 index 000000000..9f3844b15 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/dmcache.go @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dmcache + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("dmcache", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 10, + }, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *DmCache { + return &DmCache{ + Config: Config{ + Timeout: web.Duration(time.Second * 2), + }, + charts: &module.Charts{}, + devices: make(map[string]bool), + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` +} + +type ( + DmCache struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + exec dmsetupCLI + + devices map[string]bool + } + dmsetupCLI interface { + cacheStatus() ([]byte, error) + } +) + +func (c *DmCache) Configuration() any { + return c.Config +} + +func (c *DmCache) Init() error { + dmsetup, err := c.initDmsetupCLI() + if err != nil { + c.Errorf("dmsetup exec initialization: %v", err) + return err + } + c.exec = dmsetup + + return nil +} + +func (c *DmCache) Check() error { + mx, err := c.collect() + if err != nil { + c.Error(err) + return err + } + + if len(mx) == 0 { + return errors.New("no metrics collected") + } + + return nil +} + +func (c *DmCache) Charts() *module.Charts { + return c.charts +} + +func (c *DmCache) Collect() map[string]int64 { + mx, err := c.collect() + if err != nil { + c.Error(err) + } + + if len(mx) == 0 { + return nil + } + + return mx +} + +func (c *DmCache) Cleanup() {} diff --git a/src/go/plugin/go.d/modules/dmcache/dmcache_test.go b/src/go/plugin/go.d/modules/dmcache/dmcache_test.go new file mode 100644 index 000000000..218ae044c --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/dmcache_test.go @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dmcache + +import ( + "errors" + "os" + "testing" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + + } +} + +func TestDmCace_Configuration(t *testing.T) { + module.TestConfigurationSerialize(t, &DmCache{}, dataConfigJSON, dataConfigYAML) +} + +func TestDmCache_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "fails if failed to locate ndsudo": { + wantFail: true, + config: New().Config, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + lvm := New() + lvm.Config = test.config + + if test.wantFail { + assert.Error(t, lvm.Init()) + } else { + assert.NoError(t, lvm.Init()) + } + }) + } +} + +func TestDmCache_Cleanup(t *testing.T) { + tests := map[string]struct { + prepare func() *DmCache + }{ + "not initialized exec": { + prepare: func() *DmCache { + return New() + }, + }, + "after check": { + prepare: func() *DmCache { + lvm := New() + lvm.exec = prepareMockOK() + _ = lvm.Check() + return lvm + }, + }, + "after collect": { + prepare: func() *DmCache { + lvm := New() + lvm.exec = prepareMockOK() + _ = lvm.Collect() + return lvm + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + lvm := test.prepare() + + assert.NotPanics(t, lvm.Cleanup) + }) + } +} + +func TestDmCache_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestDmCache_Check(t *testing.T) { + tests := map[string]struct { + prepareMock func() *mockDmsetupExec + wantFail bool + }{ + "success case": { + prepareMock: prepareMockOK, + wantFail: false, + }, + "error on cache status": { + prepareMock: prepareMockErr, + wantFail: true, + }, + "empty response": { + prepareMock: prepareMockEmptyResponse, + wantFail: true, + }, + "unexpected response": { + prepareMock: prepareMockUnexpectedResponse, + wantFail: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + dmcache := New() + mock := test.prepareMock() + dmcache.exec = mock + + if test.wantFail { + assert.Error(t, dmcache.Check()) + } else { + assert.NoError(t, dmcache.Check()) + } + }) + } +} + +func TestLVM_Collect(t *testing.T) { + tests := map[string]struct { + prepareMock func() *mockDmsetupExec + wantCharts int + wantMetrics map[string]int64 + }{ + "success case": { + prepareMock: prepareMockOK, + wantCharts: len(deviceChartsTmpl) * 2, + wantMetrics: map[string]int64{ + "dmcache_device_vg_raid1_md21-media_cache_free_bytes": 1252402397184, + "dmcache_device_vg_raid1_md21-media_cache_used_bytes": 396412059648, + "dmcache_device_vg_raid1_md21-media_demotions_bytes": 0, + "dmcache_device_vg_raid1_md21-media_dirty_bytes": 0, + "dmcache_device_vg_raid1_md21-media_metadata_free_bytes": 32243712, + "dmcache_device_vg_raid1_md21-media_metadata_used_bytes": 9699328, + "dmcache_device_vg_raid1_md21-media_promotions_bytes": 48035266560, + "dmcache_device_vg_raid1_md21-media_read_hits": 82870357, + "dmcache_device_vg_raid1_md21-media_read_misses": 5499462, + "dmcache_device_vg_raid1_md21-media_write_hits": 26280342, + "dmcache_device_vg_raid1_md21-media_write_misses": 8017854, + "dmcache_device_vg_raid2_md22-media_cache_free_bytes": 1252402397184, + "dmcache_device_vg_raid2_md22-media_cache_used_bytes": 396412059648, + "dmcache_device_vg_raid2_md22-media_demotions_bytes": 0, + "dmcache_device_vg_raid2_md22-media_dirty_bytes": 0, + "dmcache_device_vg_raid2_md22-media_metadata_free_bytes": 32243712, + "dmcache_device_vg_raid2_md22-media_metadata_used_bytes": 9699328, + "dmcache_device_vg_raid2_md22-media_promotions_bytes": 48035266560, + "dmcache_device_vg_raid2_md22-media_read_hits": 82870357, + "dmcache_device_vg_raid2_md22-media_read_misses": 5499462, + "dmcache_device_vg_raid2_md22-media_write_hits": 26280342, + "dmcache_device_vg_raid2_md22-media_write_misses": 8017854, + }, + }, + "error on cache status": { + prepareMock: prepareMockErr, + wantMetrics: nil, + }, + "empty response": { + prepareMock: prepareMockEmptyResponse, + wantMetrics: nil, + }, + "unexpected response": { + prepareMock: prepareMockUnexpectedResponse, + wantMetrics: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + dmcache := New() + mock := test.prepareMock() + dmcache.exec = mock + + mx := dmcache.Collect() + + assert.Equal(t, test.wantMetrics, mx) + assert.Len(t, *dmcache.Charts(), test.wantCharts) + testMetricsHasAllChartsDims(t, dmcache, mx) + }) + } +} + +func testMetricsHasAllChartsDims(t *testing.T, dmcache *DmCache, mx map[string]int64) { + for _, chart := range *dmcache.Charts() { + if chart.Obsolete { + continue + } + for _, dim := range chart.Dims { + _, ok := mx[dim.ID] + assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID) + } + } +} + +func prepareMockOK() *mockDmsetupExec { + return &mockDmsetupExec{ + cacheStatusData: []byte(` +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 - +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 - +`), + } +} + +func prepareMockErr() *mockDmsetupExec { + return &mockDmsetupExec{ + errOnCacheStatus: true, + } +} + +func prepareMockEmptyResponse() *mockDmsetupExec { + return &mockDmsetupExec{} +} + +func prepareMockUnexpectedResponse() *mockDmsetupExec { + return &mockDmsetupExec{ + cacheStatusData: []byte(` +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus. +Fusce et felis pulvinar, posuere sem non, porttitor eros. +`), + } +} + +type mockDmsetupExec struct { + errOnCacheStatus bool + cacheStatusData []byte +} + +func (m *mockDmsetupExec) cacheStatus() ([]byte, error) { + if m.errOnCacheStatus { + return nil, errors.New("mock.cacheStatus() error") + } + + return m.cacheStatusData, nil +} diff --git a/src/go/plugin/go.d/modules/dmcache/exec.go b/src/go/plugin/go.d/modules/dmcache/exec.go new file mode 100644 index 000000000..1cd11be31 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/exec.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dmcache + +import ( + "context" + "fmt" + "os/exec" + "time" + + "github.com/netdata/netdata/go/plugins/logger" +) + +func newDmsetupExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *dmsetupExec { + return &dmsetupExec{ + Logger: log, + ndsudoPath: ndsudoPath, + timeout: timeout, + } +} + +type dmsetupExec struct { + *logger.Logger + + ndsudoPath string + timeout time.Duration +} + +func (e *dmsetupExec) cacheStatus() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), e.timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, e.ndsudoPath, "dmsetup-status-cache") + e.Debugf("executing '%s'", cmd) + + bs, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("error on '%s': %v", cmd, err) + } + + return bs, nil +} diff --git a/src/go/plugin/go.d/modules/dmcache/init.go b/src/go/plugin/go.d/modules/dmcache/init.go new file mode 100644 index 000000000..229972da7 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/init.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package dmcache + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/netdata/netdata/go/plugins/pkg/executable" +) + +func (c *DmCache) initDmsetupCLI() (dmsetupCLI, error) { + ndsudoPath := filepath.Join(executable.Directory, "ndsudo") + if _, err := os.Stat(ndsudoPath); err != nil { + return nil, fmt.Errorf("ndsudo executable not found: %v", err) + + } + + dmsetup := newDmsetupExec(ndsudoPath, c.Timeout.Duration(), c.Logger) + + return dmsetup, nil +} diff --git a/src/go/plugin/go.d/modules/dmcache/integrations/dmcache_devices.md b/src/go/plugin/go.d/modules/dmcache/integrations/dmcache_devices.md new file mode 100644 index 000000000..ac61311b9 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/integrations/dmcache_devices.md @@ -0,0 +1,198 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/dmcache/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/dmcache/metadata.yaml" +sidebar_label: "DMCache devices" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Storage, Mount Points and Filesystems" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# DMCache devices + + +<img src="https://netdata.cloud/img/filesystem.svg" width="150"/> + + +Plugin: go.d.plugin +Module: dmcache + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors DMCache, providing insights into capacity usage, efficiency, and activity. It relies on the [`dmsetup`](https://man7.org/linux/man-pages/man8/dmsetup.8.html) CLI tool but avoids directly executing the binary. Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment. This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management. + + + + +This collector is supported on all platforms. + +This collector only supports collecting metrics from a single instance of this integration. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per dmcache device + +These metrics refer to the DMCache device. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| device | Device name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| dmcache.device_cache_space_usage | free, used | bytes | +| dmcache.device_metadata_space_usage | free, used | bytes | +| dmcache.device_cache_read_efficiency | hits, misses | requests/s | +| dmcache.device_cache_write_efficiency | hits, misses | requests/s | +| dmcache.device_cache_activity | promotions, demotions | bytes/s | +| dmcache.device_cache_dirty_size | dirty | bytes | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/dmcache.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/dmcache.conf +``` +#### Options + +The following options can be defined globally: update_every. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 10 | no | +| timeout | dmsetup binary execution timeout. | 2 | no | + +</details> + +#### Examples + +##### Custom update_every + +Allows you to override the default data collection interval. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: dmcache + update_every: 5 # Collect DMCache statistics every 5 seconds + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +To troubleshoot issues with the `dmcache` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m dmcache + ``` + +### Getting Logs + +If you're encountering problems with the `dmcache` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep dmcache +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep dmcache /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep dmcache +``` + + diff --git a/src/go/plugin/go.d/modules/dmcache/metadata.yaml b/src/go/plugin/go.d/modules/dmcache/metadata.yaml new file mode 100644 index 000000000..58d9e4621 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/metadata.yaml @@ -0,0 +1,131 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-dmcache + plugin_name: go.d.plugin + module_name: dmcache + monitored_instance: + name: DMCache devices + link: "" + icon_filename: filesystem.svg + categories: + - data-collection.storage-mount-points-and-filesystems + keywords: + - dmcache + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: > + This collector monitors DMCache, providing insights into capacity usage, efficiency, and activity. + It relies on the [`dmsetup`](https://man7.org/linux/man-pages/man8/dmsetup.8.html) CLI tool but avoids directly executing the binary. + Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment. + This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management. + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: false + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: go.d/dmcache.conf + options: + description: | + The following options can be defined globally: update_every. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 10 + required: false + - name: timeout + description: dmsetup binary execution timeout. + default_value: 2 + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Custom update_every + description: Allows you to override the default data collection interval. + config: | + jobs: + - name: dmcache + update_every: 5 # Collect DMCache statistics every 5 seconds + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: dmcache device + description: These metrics refer to the DMCache device. + labels: + - name: device + description: Device name + metrics: + - name: dmcache.device_cache_space_usage + description: DMCache space usage + unit: bytes + chart_type: stacked + dimensions: + - name: free + - name: used + - name: dmcache.device_metadata_space_usage + description: DMCache metadata space usage + unit: bytes + chart_type: stacked + dimensions: + - name: free + - name: used + - name: dmcache.device_cache_read_efficiency + description: DMCache read efficiency + unit: requests/s + chart_type: stacked + dimensions: + - name: hits + - name: misses + - name: dmcache.device_cache_write_efficiency + description: DMCache write efficiency + unit: requests/s + chart_type: stacked + dimensions: + - name: hits + - name: misses + - name: dmcache.device_cache_activity + description: DMCache activity + unit: bytes/s + chart_type: area + dimensions: + - name: promotions + - name: demotions + - name: dmcache.device_cache_dirty_size + description: DMCache dirty data size + unit: bytes + chart_type: area + dimensions: + - name: dirty diff --git a/src/go/plugin/go.d/modules/dmcache/testdata/config.json b/src/go/plugin/go.d/modules/dmcache/testdata/config.json new file mode 100644 index 000000000..291ecee3d --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/testdata/config.json @@ -0,0 +1,4 @@ +{ + "update_every": 123, + "timeout": 123.123 +} diff --git a/src/go/plugin/go.d/modules/dmcache/testdata/config.yaml b/src/go/plugin/go.d/modules/dmcache/testdata/config.yaml new file mode 100644 index 000000000..25b0b4c78 --- /dev/null +++ b/src/go/plugin/go.d/modules/dmcache/testdata/config.yaml @@ -0,0 +1,2 @@ +update_every: 123 +timeout: 123.123 |