diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 08:15:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 08:15:35 +0000 |
commit | f09848204fa5283d21ea43e262ee41aa578e1808 (patch) | |
tree | c62385d7adf209fa6a798635954d887f718fb3fb /src/go/plugin/go.d/agent/module | |
parent | Releasing debian version 1.46.3-2. (diff) | |
download | netdata-f09848204fa5283d21ea43e262ee41aa578e1808.tar.xz netdata-f09848204fa5283d21ea43e262ee41aa578e1808.zip |
Merging upstream version 1.47.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/go/plugin/go.d/agent/module')
-rw-r--r-- | src/go/plugin/go.d/agent/module/charts.go | 497 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/charts_test.go | 383 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/job.go | 645 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/job_test.go | 291 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/mock.go | 94 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/mock_test.go | 54 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/module.go | 77 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/registry.go | 52 | ||||
-rw-r--r-- | src/go/plugin/go.d/agent/module/registry_test.go | 34 |
9 files changed, 2127 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/agent/module/charts.go b/src/go/plugin/go.d/agent/module/charts.go new file mode 100644 index 000000000..b60b3bac1 --- /dev/null +++ b/src/go/plugin/go.d/agent/module/charts.go @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "errors" + "fmt" + "strings" + "testing" + "unicode" + + "github.com/stretchr/testify/assert" +) + +type ( + ChartType string + DimAlgo string +) + +const ( + // Line chart type. + Line ChartType = "line" + // Area chart type. + Area ChartType = "area" + // Stacked chart type. + Stacked ChartType = "stacked" + + // Absolute dimension algorithm. + // The value is to drawn as-is (interpolated to second boundary). + Absolute DimAlgo = "absolute" + // Incremental dimension algorithm. + // The value increases over time, the difference from the last value is presented in the chart, + // the server interpolates the value and calculates a per second figure. + Incremental DimAlgo = "incremental" + // PercentOfAbsolute dimension algorithm. + // The percent of this value compared to the total of all dimensions. + PercentOfAbsolute DimAlgo = "percentage-of-absolute-row" + // PercentOfIncremental dimension algorithm. + // The percent of this value compared to the incremental total of all dimensions + PercentOfIncremental DimAlgo = "percentage-of-incremental-row" +) + +const ( + // Not documented. + // https://github.com/netdata/netdata/blob/cc2586de697702f86a3c34e60e23652dd4ddcb42/database/rrd.h#L204 + + LabelSourceAuto = 1 << 0 + LabelSourceConf = 1 << 1 + LabelSourceK8s = 1 << 2 +) + +func (d DimAlgo) String() string { + switch d { + case Absolute, Incremental, PercentOfAbsolute, PercentOfIncremental: + return string(d) + } + return string(Absolute) +} + +func (c ChartType) String() string { + switch c { + case Line, Area, Stacked: + return string(c) + } + return string(Line) +} + +type ( + // Charts is a collection of Charts. + Charts []*Chart + + // Opts represents chart options. + Opts struct { + Obsolete bool + Detail bool + StoreFirst bool + Hidden bool + } + + // Chart represents a chart. + // For the full description please visit https://docs.netdata.cloud/collectors/plugins.d/#chart + Chart struct { + // typeID is the unique identification of the chart, if not specified, + // the orchestrator will use job full name + chart ID as typeID (default behaviour). + typ string + id string + + OverModule string + IDSep bool + ID string + OverID string + Title string + Units string + Fam string + Ctx string + Type ChartType + Priority int + Opts + + Labels []Label + Dims Dims + Vars Vars + + Retries int + + remove bool + // created flag is used to indicate whether the chart needs to be created by the orchestrator. + created bool + // updated flag is used to indicate whether the chart was updated on last data collection interval. + updated bool + + // ignore flag is used to indicate that the chart shouldn't be sent to the netdata plugins.d + ignore bool + } + + Label struct { + Key string + Value string + Source int + } + + // DimOpts represents dimension options. + DimOpts struct { + Obsolete bool + Hidden bool + NoReset bool + NoOverflow bool + } + + // Dim represents a chart dimension. + // For detailed description please visit https://docs.netdata.cloud/collectors/plugins.d/#dimension. + Dim struct { + ID string + Name string + Algo DimAlgo + Mul int + Div int + DimOpts + + remove bool + } + + // Var represents a chart variable. + // For detailed description please visit https://docs.netdata.cloud/collectors/plugins.d/#variable + Var struct { + ID string + Name string + Value int64 + } + + // Dims is a collection of dims. + Dims []*Dim + // Vars is a collection of vars. + Vars []*Var +) + +func (o Opts) String() string { + var b strings.Builder + if o.Detail { + b.WriteString(" detail") + } + if o.Hidden { + b.WriteString(" hidden") + } + if o.Obsolete { + b.WriteString(" obsolete") + } + if o.StoreFirst { + b.WriteString(" store_first") + } + + if len(b.String()) == 0 { + return "" + } + return b.String()[1:] +} + +func (o DimOpts) String() string { + var b strings.Builder + if o.Hidden { + b.WriteString(" hidden") + } + if o.NoOverflow { + b.WriteString(" nooverflow") + } + if o.NoReset { + b.WriteString(" noreset") + } + if o.Obsolete { + b.WriteString(" obsolete") + } + + if len(b.String()) == 0 { + return "" + } + return b.String()[1:] +} + +// Add adds (appends) a variable number of Charts. +func (c *Charts) Add(charts ...*Chart) error { + for _, chart := range charts { + err := checkChart(chart) + if err != nil { + return fmt.Errorf("error on adding chart '%s' : %s", chart.ID, err) + } + if chart := c.Get(chart.ID); chart != nil && !chart.remove { + return fmt.Errorf("error on adding chart : '%s' is already in charts", chart.ID) + } + *c = append(*c, chart) + } + + return nil +} + +// Get returns the chart by ID. +func (c Charts) Get(chartID string) *Chart { + idx := c.index(chartID) + if idx == -1 { + return nil + } + return c[idx] +} + +// Has returns true if ChartsFunc contain the chart with the given ID, false otherwise. +func (c Charts) Has(chartID string) bool { + return c.index(chartID) != -1 +} + +// Remove removes the chart from Charts by ID. +// Avoid to use it in runtime. +func (c *Charts) Remove(chartID string) error { + idx := c.index(chartID) + if idx == -1 { + return fmt.Errorf("error on removing chart : '%s' is not in charts", chartID) + } + copy((*c)[idx:], (*c)[idx+1:]) + (*c)[len(*c)-1] = nil + *c = (*c)[:len(*c)-1] + return nil +} + +// Copy returns a deep copy of ChartsFunc. +func (c Charts) Copy() *Charts { + charts := Charts{} + for idx := range c { + charts = append(charts, c[idx].Copy()) + } + return &charts +} + +func (c Charts) index(chartID string) int { + for idx := range c { + if c[idx].ID == chartID { + return idx + } + } + return -1 +} + +// MarkNotCreated changes 'created' chart flag to false. +// Use it to add dimension in runtime. +func (c *Chart) MarkNotCreated() { + c.created = false +} + +// MarkRemove sets 'remove' flag and Obsolete option to true. +// Use it to remove chart in runtime. +func (c *Chart) MarkRemove() { + c.Obsolete = true + c.remove = true +} + +// MarkDimRemove sets 'remove' flag, Obsolete and optionally Hidden options to true. +// Use it to remove dimension in runtime. +func (c *Chart) MarkDimRemove(dimID string, hide bool) error { + if !c.HasDim(dimID) { + return fmt.Errorf("chart '%s' has no '%s' dimension", c.ID, dimID) + } + dim := c.GetDim(dimID) + dim.Obsolete = true + if hide { + dim.Hidden = true + } + dim.remove = true + return nil +} + +// AddDim adds new dimension to the chart dimensions. +func (c *Chart) AddDim(newDim *Dim) error { + err := checkDim(newDim) + if err != nil { + return fmt.Errorf("error on adding dim to chart '%s' : %s", c.ID, err) + } + if c.HasDim(newDim.ID) { + return fmt.Errorf("error on adding dim : '%s' is already in chart '%s' dims", newDim.ID, c.ID) + } + c.Dims = append(c.Dims, newDim) + + return nil +} + +// AddVar adds new variable to the chart variables. +func (c *Chart) AddVar(newVar *Var) error { + err := checkVar(newVar) + if err != nil { + return fmt.Errorf("error on adding var to chart '%s' : %s", c.ID, err) + } + if c.indexVar(newVar.ID) != -1 { + return fmt.Errorf("error on adding var : '%s' is already in chart '%s' vars", newVar.ID, c.ID) + } + c.Vars = append(c.Vars, newVar) + + return nil +} + +// GetDim returns dimension by ID. +func (c *Chart) GetDim(dimID string) *Dim { + idx := c.indexDim(dimID) + if idx == -1 { + return nil + } + return c.Dims[idx] +} + +// RemoveDim removes dimension by ID. +// Avoid to use it in runtime. +func (c *Chart) RemoveDim(dimID string) error { + idx := c.indexDim(dimID) + if idx == -1 { + return fmt.Errorf("error on removing dim : '%s' isn't in chart '%s'", dimID, c.ID) + } + c.Dims = append(c.Dims[:idx], c.Dims[idx+1:]...) + + return nil +} + +// HasDim returns true if the chart contains dimension with the given ID, false otherwise. +func (c Chart) HasDim(dimID string) bool { + return c.indexDim(dimID) != -1 +} + +// Copy returns a deep copy of the chart. +func (c Chart) Copy() *Chart { + chart := c + chart.Dims = Dims{} + chart.Vars = Vars{} + + for idx := range c.Dims { + chart.Dims = append(chart.Dims, c.Dims[idx].copy()) + } + for idx := range c.Vars { + chart.Vars = append(chart.Vars, c.Vars[idx].copy()) + } + + return &chart +} + +func (c Chart) indexDim(dimID string) int { + for idx := range c.Dims { + if c.Dims[idx].ID == dimID { + return idx + } + } + return -1 +} + +func (c Chart) indexVar(varID string) int { + for idx := range c.Vars { + if c.Vars[idx].ID == varID { + return idx + } + } + return -1 +} + +func (d Dim) copy() *Dim { + return &d +} + +func (v Var) copy() *Var { + return &v +} + +func checkCharts(charts ...*Chart) error { + for _, chart := range charts { + err := checkChart(chart) + if err != nil { + return fmt.Errorf("chart '%s' : %v", chart.ID, err) + } + } + return nil +} + +func checkChart(chart *Chart) error { + if chart.ID == "" { + return errors.New("empty ID") + } + + if chart.Title == "" { + return errors.New("empty Title") + } + + if chart.Units == "" { + return errors.New("empty Units") + } + + if id := checkID(chart.ID); id != -1 { + return fmt.Errorf("unacceptable symbol in ID : '%c'", id) + } + + set := make(map[string]bool) + + for _, d := range chart.Dims { + err := checkDim(d) + if err != nil { + return err + } + if set[d.ID] { + return fmt.Errorf("duplicate dim '%s'", d.ID) + } + set[d.ID] = true + } + + set = make(map[string]bool) + + for _, v := range chart.Vars { + if err := checkVar(v); err != nil { + return err + } + if set[v.ID] { + return fmt.Errorf("duplicate var '%s'", v.ID) + } + set[v.ID] = true + } + return nil +} + +func checkDim(d *Dim) error { + if d.ID == "" { + return errors.New("empty dim ID") + } + if id := checkID(d.ID); id != -1 && (d.Name == "" || checkID(d.Name) != -1) { + return fmt.Errorf("unacceptable symbol in dim ID '%s' : '%c'", d.ID, id) + } + return nil +} + +func checkVar(v *Var) error { + if v.ID == "" { + return errors.New("empty var ID") + } + if id := checkID(v.ID); id != -1 { + return fmt.Errorf("unacceptable symbol in var ID '%s' : '%c'", v.ID, id) + } + return nil +} + +func checkID(id string) int { + for _, r := range id { + if unicode.IsSpace(r) { + return int(r) + } + } + return -1 +} + +func TestMetricsHasAllChartsDims(t *testing.T, charts *Charts, mx map[string]int64) { + for _, chart := range *charts { + if chart.Obsolete { + continue + } + for _, dim := range chart.Dims { + _, ok := mx[dim.ID] + assert.Truef(t, ok, "missing data for dimension '%s' in chart '%s'", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := mx[v.ID] + assert.Truef(t, ok, "missing data for variable '%s' in chart '%s'", v.ID, chart.ID) + } + } +} + +func TestMetricsHasAllChartsDimsSkip(t *testing.T, charts *Charts, mx map[string]int64, skip func(chart *Chart) bool) { + for _, chart := range *charts { + if chart.Obsolete || (skip != nil && skip(chart)) { + continue + } + for _, dim := range chart.Dims { + _, ok := mx[dim.ID] + assert.Truef(t, ok, "missing data for dimension '%s' in chart '%s'", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := mx[v.ID] + assert.Truef(t, ok, "missing data for variable '%s' in chart '%s'", v.ID, chart.ID) + } + } +} diff --git a/src/go/plugin/go.d/agent/module/charts_test.go b/src/go/plugin/go.d/agent/module/charts_test.go new file mode 100644 index 000000000..b0dcf806f --- /dev/null +++ b/src/go/plugin/go.d/agent/module/charts_test.go @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestChart(id string) *Chart { + return &Chart{ + ID: id, + Title: "Title", + Units: "units", + Fam: "family", + Ctx: "context", + Type: Line, + Dims: Dims{ + {ID: "dim1", Algo: Absolute}, + }, + Vars: Vars{ + {ID: "var1", Value: 1}, + }, + } +} + +func TestDimAlgo_String(t *testing.T) { + cases := []struct { + expected string + actual fmt.Stringer + }{ + {"absolute", Absolute}, + {"incremental", Incremental}, + {"percentage-of-absolute-row", PercentOfAbsolute}, + {"percentage-of-incremental-row", PercentOfIncremental}, + {"absolute", DimAlgo("wrong")}, + } + + for _, v := range cases { + assert.Equal(t, v.expected, v.actual.String()) + } +} + +func TestChartType_String(t *testing.T) { + cases := []struct { + expected string + actual fmt.Stringer + }{ + {"line", Line}, + {"area", Area}, + {"stacked", Stacked}, + {"line", ChartType("wrong")}, + } + + for _, v := range cases { + assert.Equal(t, v.expected, v.actual.String()) + } +} + +func TestOpts_String(t *testing.T) { + cases := []struct { + expected string + actual fmt.Stringer + }{ + {"", Opts{}}, + { + "detail hidden obsolete store_first", + Opts{Detail: true, Hidden: true, Obsolete: true, StoreFirst: true}, + }, + { + "detail hidden obsolete store_first", + Opts{Detail: true, Hidden: true, Obsolete: true, StoreFirst: true}, + }, + } + + for _, v := range cases { + assert.Equal(t, v.expected, v.actual.String()) + } +} + +func TestDimOpts_String(t *testing.T) { + cases := []struct { + expected string + actual fmt.Stringer + }{ + {"", DimOpts{}}, + { + "hidden nooverflow noreset obsolete", + DimOpts{Hidden: true, NoOverflow: true, NoReset: true, Obsolete: true}, + }, + { + "hidden obsolete", + DimOpts{Hidden: true, NoOverflow: false, NoReset: false, Obsolete: true}, + }, + } + + for _, v := range cases { + assert.Equal(t, v.expected, v.actual.String()) + } +} + +func TestCharts_Copy(t *testing.T) { + orig := &Charts{ + createTestChart("1"), + createTestChart("2"), + } + copied := orig.Copy() + + require.False(t, orig == copied, "Charts copy points to the same address") + require.Len(t, *orig, len(*copied)) + + for idx := range *orig { + compareCharts(t, (*orig)[idx], (*copied)[idx]) + } +} + +func TestChart_Copy(t *testing.T) { + orig := createTestChart("1") + + compareCharts(t, orig, orig.Copy()) +} + +func TestCharts_Add(t *testing.T) { + charts := Charts{} + chart1 := createTestChart("1") + chart2 := createTestChart("2") + chart3 := createTestChart("") + + // OK case + assert.NoError(t, charts.Add( + chart1, + chart2, + )) + assert.Len(t, charts, 2) + + // NG case + assert.Error(t, charts.Add( + chart3, + chart1, + chart2, + )) + assert.Len(t, charts, 2) + + assert.True(t, charts[0] == chart1) + assert.True(t, charts[1] == chart2) +} + +func TestCharts_Add_SameID(t *testing.T) { + charts := Charts{} + chart1 := createTestChart("1") + chart2 := createTestChart("1") + + assert.NoError(t, charts.Add(chart1)) + assert.Error(t, charts.Add(chart2)) + assert.Len(t, charts, 1) + + charts = Charts{} + chart1 = createTestChart("1") + chart2 = createTestChart("1") + + assert.NoError(t, charts.Add(chart1)) + chart1.MarkRemove() + assert.NoError(t, charts.Add(chart2)) + assert.Len(t, charts, 2) +} + +func TestCharts_Get(t *testing.T) { + chart := createTestChart("1") + charts := Charts{ + chart, + } + + // OK case + assert.True(t, chart == charts.Get("1")) + // NG case + assert.Nil(t, charts.Get("2")) +} + +func TestCharts_Has(t *testing.T) { + chart := createTestChart("1") + charts := &Charts{ + chart, + } + + // OK case + assert.True(t, charts.Has("1")) + // NG case + assert.False(t, charts.Has("2")) +} + +func TestCharts_Remove(t *testing.T) { + chart := createTestChart("1") + charts := &Charts{ + chart, + } + + // OK case + assert.NoError(t, charts.Remove("1")) + assert.Len(t, *charts, 0) + + // NG case + assert.Error(t, charts.Remove("2")) +} + +func TestChart_AddDim(t *testing.T) { + chart := createTestChart("1") + dim := &Dim{ID: "dim2"} + + // OK case + assert.NoError(t, chart.AddDim(dim)) + assert.Len(t, chart.Dims, 2) + + // NG case + assert.Error(t, chart.AddDim(dim)) + assert.Len(t, chart.Dims, 2) +} + +func TestChart_AddVar(t *testing.T) { + chart := createTestChart("1") + variable := &Var{ID: "var2"} + + // OK case + assert.NoError(t, chart.AddVar(variable)) + assert.Len(t, chart.Vars, 2) + + // NG case + assert.Error(t, chart.AddVar(variable)) + assert.Len(t, chart.Vars, 2) +} + +func TestChart_GetDim(t *testing.T) { + chart := &Chart{ + Dims: Dims{ + {ID: "1"}, + {ID: "2"}, + }, + } + + // OK case + assert.True(t, chart.GetDim("1") != nil && chart.GetDim("1").ID == "1") + + // NG case + assert.Nil(t, chart.GetDim("3")) +} + +func TestChart_RemoveDim(t *testing.T) { + chart := createTestChart("1") + + // OK case + assert.NoError(t, chart.RemoveDim("dim1")) + assert.Len(t, chart.Dims, 0) + + // NG case + assert.Error(t, chart.RemoveDim("dim2")) +} + +func TestChart_HasDim(t *testing.T) { + chart := createTestChart("1") + + // OK case + assert.True(t, chart.HasDim("dim1")) + // NG case + assert.False(t, chart.HasDim("dim2")) +} + +func TestChart_MarkNotCreated(t *testing.T) { + chart := createTestChart("1") + + chart.MarkNotCreated() + assert.False(t, chart.created) +} + +func TestChart_MarkRemove(t *testing.T) { + chart := createTestChart("1") + + chart.MarkRemove() + assert.True(t, chart.remove) + assert.True(t, chart.Obsolete) +} + +func TestChart_MarkDimRemove(t *testing.T) { + chart := createTestChart("1") + + assert.Error(t, chart.MarkDimRemove("dim99", false)) + assert.NoError(t, chart.MarkDimRemove("dim1", true)) + assert.True(t, chart.GetDim("dim1").Obsolete) + assert.True(t, chart.GetDim("dim1").Hidden) + assert.True(t, chart.GetDim("dim1").remove) +} + +func TestChart_check(t *testing.T) { + // OK case + chart := createTestChart("1") + assert.NoError(t, checkChart(chart)) + + // NG case + chart = createTestChart("1") + chart.ID = "" + assert.Error(t, checkChart(chart)) + + chart = createTestChart("1") + chart.ID = "invalid id" + assert.Error(t, checkChart(chart)) + + chart = createTestChart("1") + chart.Title = "" + assert.Error(t, checkChart(chart)) + + chart = createTestChart("1") + chart.Units = "" + assert.Error(t, checkChart(chart)) + + chart = createTestChart("1") + chart.Dims = Dims{ + {ID: "1"}, + {ID: "1"}, + } + assert.Error(t, checkChart(chart)) + + chart = createTestChart("1") + chart.Vars = Vars{ + {ID: "1"}, + {ID: "1"}, + } + assert.Error(t, checkChart(chart)) +} + +func TestDim_check(t *testing.T) { + // OK case + dim := &Dim{ID: "id"} + assert.NoError(t, checkDim(dim)) + + // NG case + dim = &Dim{ID: "id"} + dim.ID = "" + assert.Error(t, checkDim(dim)) + + dim = &Dim{ID: "id"} + dim.ID = "invalid id" + assert.Error(t, checkDim(dim)) + + dim = &Dim{ID: "i d", Name: "id"} + assert.NoError(t, checkDim(dim)) +} + +func TestVar_check(t *testing.T) { + // OK case + v := &Var{ID: "id"} + assert.NoError(t, checkVar(v)) + + // NG case + v = &Var{ID: "id"} + v.ID = "" + assert.Error(t, checkVar(v)) + + v = &Var{ID: "id"} + v.ID = "invalid id" + assert.Error(t, checkVar(v)) +} + +func compareCharts(t *testing.T, orig, copied *Chart) { + // 1. compare chart pointers + // 2. compare Dims, Vars length + // 3. compare Dims, Vars pointers + + assert.False(t, orig == copied, "Chart copy ChartsFunc points to the same address") + + require.Len(t, orig.Dims, len(copied.Dims)) + require.Len(t, orig.Vars, len(copied.Vars)) + + for idx := range (*orig).Dims { + assert.False(t, orig.Dims[idx] == copied.Dims[idx], "Chart copy dim points to the same address") + assert.Equal(t, orig.Dims[idx], copied.Dims[idx], "Chart copy dim isn't equal to orig") + } + + for idx := range (*orig).Vars { + assert.False(t, orig.Vars[idx] == copied.Vars[idx], "Chart copy var points to the same address") + assert.Equal(t, orig.Vars[idx], copied.Vars[idx], "Chart copy var isn't equal to orig") + } +} diff --git a/src/go/plugin/go.d/agent/module/job.go b/src/go/plugin/go.d/agent/module/job.go new file mode 100644 index 000000000..67fae8aa2 --- /dev/null +++ b/src/go/plugin/go.d/agent/module/job.go @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "bytes" + "errors" + "fmt" + "io" + "log/slog" + "os" + "regexp" + "runtime/debug" + "strings" + "sync" + "time" + + "github.com/netdata/netdata/go/plugins/logger" + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/netdataapi" + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes" +) + +var obsoleteLock = &sync.Mutex{} +var obsoleteCharts = true + +func DontObsoleteCharts() { + obsoleteLock.Lock() + obsoleteCharts = false + obsoleteLock.Unlock() +} + +func shouldObsoleteCharts() bool { + obsoleteLock.Lock() + defer obsoleteLock.Unlock() + return obsoleteCharts +} + +var reSpace = regexp.MustCompile(`\s+`) + +var ndInternalMonitoringDisabled = os.Getenv("NETDATA_INTERNALS_MONITORING") == "NO" + +func newRuntimeChart(pluginName string) *Chart { + // this is needed to keep the same name as we had before https://github.com/netdata/netdata/go/plugins/plugin/go.d/issues/650 + ctxName := pluginName + if ctxName == "go.d" { + ctxName = "go" + } + ctxName = reSpace.ReplaceAllString(ctxName, "_") + return &Chart{ + typ: "netdata", + Title: "Execution time", + Units: "ms", + Fam: pluginName, + Ctx: fmt.Sprintf("netdata.%s_plugin_execution_time", ctxName), + Priority: 145000, + Dims: Dims{ + {ID: "time"}, + }, + } +} + +type JobConfig struct { + PluginName string + Name string + ModuleName string + FullName string + Module Module + Labels map[string]string + Out io.Writer + UpdateEvery int + AutoDetectEvery int + Priority int + IsStock bool + + VnodeGUID string + VnodeHostname string + VnodeLabels map[string]string +} + +const ( + penaltyStep = 5 + maxPenalty = 600 + infTries = -1 +) + +func NewJob(cfg JobConfig) *Job { + var buf bytes.Buffer + + if cfg.UpdateEvery == 0 { + cfg.UpdateEvery = 1 + } + + j := &Job{ + AutoDetectEvery: cfg.AutoDetectEvery, + AutoDetectTries: infTries, + + pluginName: cfg.PluginName, + name: cfg.Name, + moduleName: cfg.ModuleName, + fullName: cfg.FullName, + updateEvery: cfg.UpdateEvery, + priority: cfg.Priority, + isStock: cfg.IsStock, + module: cfg.Module, + labels: cfg.Labels, + out: cfg.Out, + runChart: newRuntimeChart(cfg.PluginName), + stop: make(chan struct{}), + tick: make(chan int), + buf: &buf, + api: netdataapi.New(&buf), + + vnodeGUID: cfg.VnodeGUID, + vnodeHostname: cfg.VnodeHostname, + vnodeLabels: cfg.VnodeLabels, + } + + log := logger.New().With( + slog.String("collector", j.ModuleName()), + slog.String("job", j.Name()), + ) + + j.Logger = log + if j.module != nil { + j.module.GetBase().Logger = log + } + + return j +} + +// Job represents a job. It's a module wrapper. +type Job struct { + pluginName string + name string + moduleName string + fullName string + + updateEvery int + AutoDetectEvery int + AutoDetectTries int + priority int + labels map[string]string + + *logger.Logger + + isStock bool + + module Module + + initialized bool + panicked bool + + runChart *Chart + charts *Charts + tick chan int + out io.Writer + buf *bytes.Buffer + api *netdataapi.API + + retries int + prevRun time.Time + + stop chan struct{} + + vnodeCreated bool + vnodeGUID string + vnodeHostname string + vnodeLabels map[string]string +} + +// NetdataChartIDMaxLength is the chart ID max length. See RRD_ID_LENGTH_MAX in the netdata source code. +const NetdataChartIDMaxLength = 1200 + +// FullName returns job full name. +func (j *Job) FullName() string { + return j.fullName +} + +// ModuleName returns job module name. +func (j *Job) ModuleName() string { + return j.moduleName +} + +// Name returns job name. +func (j *Job) Name() string { + return j.name +} + +// Panicked returns 'panicked' flag value. +func (j *Job) Panicked() bool { + return j.panicked +} + +// AutoDetectionEvery returns value of AutoDetectEvery. +func (j *Job) AutoDetectionEvery() int { + return j.AutoDetectEvery +} + +// RetryAutoDetection returns whether it is needed to retry autodetection. +func (j *Job) RetryAutoDetection() bool { + return j.AutoDetectEvery > 0 && (j.AutoDetectTries == infTries || j.AutoDetectTries > 0) +} + +func (j *Job) Configuration() any { + return j.module.Configuration() +} + +// AutoDetection invokes init, check and postCheck. It handles panic. +func (j *Job) AutoDetection() (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic %v", err) + j.panicked = true + j.disableAutoDetection() + + j.Errorf("PANIC %v", r) + if logger.Level.Enabled(slog.LevelDebug) { + j.Errorf("STACK: %s", debug.Stack()) + } + } + if err != nil { + j.module.Cleanup() + } + }() + + if j.isStock { + j.Mute() + } + + if err = j.init(); err != nil { + j.Error("init failed") + j.Unmute() + j.disableAutoDetection() + return err + } + + if err = j.check(); err != nil { + j.Error("check failed") + j.Unmute() + return err + } + + j.Unmute() + j.Info("check success") + + if err = j.postCheck(); err != nil { + j.Error("postCheck failed") + j.disableAutoDetection() + return err + } + + return nil +} + +// Tick Tick. +func (j *Job) Tick(clock int) { + select { + case j.tick <- clock: + default: + j.Debug("skip the tick due to previous run hasn't been finished") + } +} + +// Start starts job main loop. +func (j *Job) Start() { + j.Infof("started, data collection interval %ds", j.updateEvery) + defer func() { j.Info("stopped") }() + +LOOP: + for { + select { + case <-j.stop: + break LOOP + case t := <-j.tick: + if t%(j.updateEvery+j.penalty()) == 0 { + j.runOnce() + } + } + } + j.module.Cleanup() + j.Cleanup() + j.stop <- struct{}{} +} + +// Stop stops job main loop. It blocks until the job is stopped. +func (j *Job) Stop() { + // TODO: should have blocking and non blocking stop + j.stop <- struct{}{} + <-j.stop +} + +func (j *Job) disableAutoDetection() { + j.AutoDetectEvery = 0 +} + +func (j *Job) Cleanup() { + j.buf.Reset() + if !shouldObsoleteCharts() { + return + } + + if !vnodes.Disabled { + if !j.vnodeCreated && j.vnodeGUID != "" { + _ = j.api.HOSTINFO(j.vnodeGUID, j.vnodeHostname, j.vnodeLabels) + j.vnodeCreated = true + } + _ = j.api.HOST(j.vnodeGUID) + } + + if j.runChart.created { + j.runChart.MarkRemove() + j.createChart(j.runChart) + } + if j.charts != nil { + for _, chart := range *j.charts { + if chart.created { + chart.MarkRemove() + j.createChart(chart) + } + } + } + + if j.buf.Len() > 0 { + _, _ = io.Copy(j.out, j.buf) + } +} + +func (j *Job) init() error { + if j.initialized { + return nil + } + + if err := j.module.Init(); err != nil { + return err + } + + j.initialized = true + + return nil +} + +func (j *Job) check() error { + if err := j.module.Check(); err != nil { + if j.AutoDetectTries != infTries { + j.AutoDetectTries-- + } + return err + } + return nil +} + +func (j *Job) postCheck() error { + if j.charts = j.module.Charts(); j.charts == nil { + j.Error("nil charts") + return errors.New("nil charts") + } + if err := checkCharts(*j.charts...); err != nil { + j.Errorf("charts check: %v", err) + return err + } + return nil +} + +func (j *Job) runOnce() { + curTime := time.Now() + sinceLastRun := calcSinceLastRun(curTime, j.prevRun) + j.prevRun = curTime + + metrics := j.collect() + + if j.panicked { + return + } + + if j.processMetrics(metrics, curTime, sinceLastRun) { + j.retries = 0 + } else { + j.retries++ + } + + _, _ = io.Copy(j.out, j.buf) + j.buf.Reset() +} + +func (j *Job) collect() (result map[string]int64) { + j.panicked = false + defer func() { + if r := recover(); r != nil { + j.panicked = true + j.Errorf("PANIC: %v", r) + if logger.Level.Enabled(slog.LevelDebug) { + j.Errorf("STACK: %s", debug.Stack()) + } + } + }() + return j.module.Collect() +} + +func (j *Job) processMetrics(metrics map[string]int64, startTime time.Time, sinceLastRun int) bool { + if !vnodes.Disabled { + if !j.vnodeCreated && j.vnodeGUID != "" { + _ = j.api.HOSTINFO(j.vnodeGUID, j.vnodeHostname, j.vnodeLabels) + j.vnodeCreated = true + } + + _ = j.api.HOST(j.vnodeGUID) + } + + if !ndInternalMonitoringDisabled && !j.runChart.created { + j.runChart.ID = fmt.Sprintf("execution_time_of_%s", j.FullName()) + j.createChart(j.runChart) + } + + elapsed := int64(durationTo(time.Since(startTime), time.Millisecond)) + + var i, updated int + for _, chart := range *j.charts { + if !chart.created { + typeID := fmt.Sprintf("%s.%s", j.FullName(), chart.ID) + if len(typeID) >= NetdataChartIDMaxLength { + j.Warningf("chart 'type.id' length (%d) >= max allowed (%d), the chart is ignored (%s)", + len(typeID), NetdataChartIDMaxLength, typeID) + chart.ignore = true + } + j.createChart(chart) + } + if chart.remove { + continue + } + (*j.charts)[i] = chart + i++ + if len(metrics) == 0 || chart.Obsolete { + continue + } + if j.updateChart(chart, metrics, sinceLastRun) { + updated++ + } + } + *j.charts = (*j.charts)[:i] + + if updated == 0 { + return false + } + if !ndInternalMonitoringDisabled { + j.updateChart(j.runChart, map[string]int64{"time": elapsed}, sinceLastRun) + } + + return true +} + +func (j *Job) createChart(chart *Chart) { + defer func() { chart.created = true }() + if chart.ignore { + return + } + + if chart.Priority == 0 { + chart.Priority = j.priority + j.priority++ + } + _ = j.api.CHART( + getChartType(chart, j), + getChartID(chart), + chart.OverID, + chart.Title, + chart.Units, + chart.Fam, + chart.Ctx, + chart.Type.String(), + chart.Priority, + j.updateEvery, + chart.Opts.String(), + j.pluginName, + j.moduleName, + ) + + if chart.Obsolete { + _ = j.api.EMPTYLINE() + return + } + + seen := make(map[string]bool) + for _, l := range chart.Labels { + if l.Key != "" { + seen[l.Key] = true + ls := l.Source + // the default should be auto + // https://github.com/netdata/netdata/blob/cc2586de697702f86a3c34e60e23652dd4ddcb42/database/rrd.h#L205 + if ls == 0 { + ls = LabelSourceAuto + } + _ = j.api.CLABEL(l.Key, l.Value, ls) + } + } + for k, v := range j.labels { + if !seen[k] { + _ = j.api.CLABEL(k, v, LabelSourceConf) + } + } + _ = j.api.CLABEL("_collect_job", j.Name(), LabelSourceAuto) + _ = j.api.CLABELCOMMIT() + + for _, dim := range chart.Dims { + _ = j.api.DIMENSION( + firstNotEmpty(dim.Name, dim.ID), + dim.Name, + dim.Algo.String(), + handleZero(dim.Mul), + handleZero(dim.Div), + dim.DimOpts.String(), + ) + } + for _, v := range chart.Vars { + if v.Name != "" { + _ = j.api.VARIABLE(v.Name, v.Value) + } else { + _ = j.api.VARIABLE(v.ID, v.Value) + } + } + _ = j.api.EMPTYLINE() +} + +func (j *Job) updateChart(chart *Chart, collected map[string]int64, sinceLastRun int) bool { + if chart.ignore { + dims := chart.Dims[:0] + for _, dim := range chart.Dims { + if !dim.remove { + dims = append(dims, dim) + } + } + chart.Dims = dims + return false + } + + if !chart.updated { + sinceLastRun = 0 + } + + _ = j.api.BEGIN( + getChartType(chart, j), + getChartID(chart), + sinceLastRun, + ) + var i, updated int + for _, dim := range chart.Dims { + if dim.remove { + continue + } + chart.Dims[i] = dim + i++ + if v, ok := collected[dim.ID]; !ok { + _ = j.api.SETEMPTY(firstNotEmpty(dim.Name, dim.ID)) + } else { + _ = j.api.SET(firstNotEmpty(dim.Name, dim.ID), v) + updated++ + } + } + chart.Dims = chart.Dims[:i] + + for _, vr := range chart.Vars { + if v, ok := collected[vr.ID]; ok { + if vr.Name != "" { + _ = j.api.VARIABLE(vr.Name, v) + } else { + _ = j.api.VARIABLE(vr.ID, v) + } + } + + } + _ = j.api.END() + + if chart.updated = updated > 0; chart.updated { + chart.Retries = 0 + } else { + chart.Retries++ + } + return chart.updated +} + +func (j *Job) penalty() int { + v := j.retries / penaltyStep * penaltyStep * j.updateEvery / 2 + if v > maxPenalty { + return maxPenalty + } + return v +} + +func getChartType(chart *Chart, j *Job) string { + if chart.typ != "" { + return chart.typ + } + if !chart.IDSep { + chart.typ = j.FullName() + } else if i := strings.IndexByte(chart.ID, '.'); i != -1 { + chart.typ = j.FullName() + "_" + chart.ID[:i] + } else { + chart.typ = j.FullName() + } + if chart.OverModule != "" { + if v := strings.TrimPrefix(chart.typ, j.ModuleName()); v != chart.typ { + chart.typ = chart.OverModule + v + } + } + return chart.typ +} + +func getChartID(chart *Chart) string { + if chart.id != "" { + return chart.id + } + if !chart.IDSep { + return chart.ID + } + if i := strings.IndexByte(chart.ID, '.'); i != -1 { + chart.id = chart.ID[i+1:] + } else { + chart.id = chart.ID + } + return chart.id +} + +func calcSinceLastRun(curTime, prevRun time.Time) int { + if prevRun.IsZero() { + return 0 + } + return int((curTime.UnixNano() - prevRun.UnixNano()) / 1000) +} + +func durationTo(duration time.Duration, to time.Duration) int { + return int(int64(duration) / (int64(to) / int64(time.Nanosecond))) +} + +func firstNotEmpty(val1, val2 string) string { + if val1 != "" { + return val1 + } + return val2 +} + +func handleZero(v int) int { + if v == 0 { + return 1 + } + return v +} diff --git a/src/go/plugin/go.d/agent/module/job_test.go b/src/go/plugin/go.d/agent/module/job_test.go new file mode 100644 index 000000000..c87f840d5 --- /dev/null +++ b/src/go/plugin/go.d/agent/module/job_test.go @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "errors" + "fmt" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const ( + pluginName = "plugin" + modName = "module" + jobName = "job" +) + +func newTestJob() *Job { + return NewJob( + JobConfig{ + PluginName: pluginName, + Name: jobName, + ModuleName: modName, + FullName: modName + "_" + jobName, + Module: nil, + Out: io.Discard, + UpdateEvery: 0, + AutoDetectEvery: 0, + Priority: 0, + }, + ) +} + +func TestNewJob(t *testing.T) { + assert.IsType(t, (*Job)(nil), newTestJob()) +} + +func TestJob_FullName(t *testing.T) { + job := newTestJob() + + assert.Equal(t, job.FullName(), fmt.Sprintf("%s_%s", modName, jobName)) +} + +func TestJob_ModuleName(t *testing.T) { + job := newTestJob() + + assert.Equal(t, job.ModuleName(), modName) +} + +func TestJob_Name(t *testing.T) { + job := newTestJob() + + assert.Equal(t, job.Name(), jobName) +} + +func TestJob_Panicked(t *testing.T) { + job := newTestJob() + + assert.Equal(t, job.Panicked(), job.panicked) + job.panicked = true + assert.Equal(t, job.Panicked(), job.panicked) +} + +func TestJob_AutoDetectionEvery(t *testing.T) { + job := newTestJob() + + assert.Equal(t, job.AutoDetectionEvery(), job.AutoDetectEvery) +} + +func TestJob_RetryAutoDetection(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + return nil + }, + CheckFunc: func() error { return errors.New("check error") }, + ChartsFunc: func() *Charts { + return &Charts{} + }, + } + job.module = m + job.AutoDetectEvery = 1 + + assert.True(t, job.RetryAutoDetection()) + assert.Equal(t, infTries, job.AutoDetectTries) + for i := 0; i < 1000; i++ { + _ = job.check() + } + assert.True(t, job.RetryAutoDetection()) + assert.Equal(t, infTries, job.AutoDetectTries) + + job.AutoDetectTries = 10 + for i := 0; i < 10; i++ { + _ = job.check() + } + assert.False(t, job.RetryAutoDetection()) + assert.Equal(t, 0, job.AutoDetectTries) +} + +func TestJob_AutoDetection(t *testing.T) { + job := newTestJob() + var v int + m := &MockModule{ + InitFunc: func() error { + v++ + return nil + }, + CheckFunc: func() error { + v++ + return nil + }, + ChartsFunc: func() *Charts { + v++ + return &Charts{} + }, + } + job.module = m + + assert.NoError(t, job.AutoDetection()) + assert.Equal(t, 3, v) +} + +func TestJob_AutoDetection_FailInit(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + return errors.New("init error") + }, + } + job.module = m + + assert.Error(t, job.AutoDetection()) + assert.True(t, m.CleanupDone) +} + +func TestJob_AutoDetection_FailCheck(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + return nil + }, + CheckFunc: func() error { + return errors.New("check error") + }, + } + job.module = m + + assert.Error(t, job.AutoDetection()) + assert.True(t, m.CleanupDone) +} + +func TestJob_AutoDetection_FailPostCheck(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + return nil + }, + CheckFunc: func() error { + return nil + }, + ChartsFunc: func() *Charts { + return nil + }, + } + job.module = m + + assert.Error(t, job.AutoDetection()) + assert.True(t, m.CleanupDone) +} + +func TestJob_AutoDetection_PanicInit(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + panic("panic in Init") + }, + } + job.module = m + + assert.Error(t, job.AutoDetection()) + assert.True(t, m.CleanupDone) +} + +func TestJob_AutoDetection_PanicCheck(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + return nil + }, + CheckFunc: func() error { + panic("panic in Check") + }, + } + job.module = m + + assert.Error(t, job.AutoDetection()) + assert.True(t, m.CleanupDone) +} + +func TestJob_AutoDetection_PanicPostCheck(t *testing.T) { + job := newTestJob() + m := &MockModule{ + InitFunc: func() error { + return nil + }, + CheckFunc: func() error { + return nil + }, + ChartsFunc: func() *Charts { + panic("panic in PostCheck") + }, + } + job.module = m + + assert.Error(t, job.AutoDetection()) + assert.True(t, m.CleanupDone) +} + +func TestJob_Start(t *testing.T) { + m := &MockModule{ + ChartsFunc: func() *Charts { + return &Charts{ + &Chart{ + ID: "id", + Title: "title", + Units: "units", + Dims: Dims{ + {ID: "id1"}, + {ID: "id2"}, + }, + }, + } + }, + CollectFunc: func() map[string]int64 { + return map[string]int64{ + "id1": 1, + "id2": 2, + } + }, + } + job := newTestJob() + job.module = m + job.charts = job.module.Charts() + job.updateEvery = 1 + + go func() { + for i := 1; i < 3; i++ { + job.Tick(i) + time.Sleep(time.Second) + } + job.Stop() + }() + + job.Start() + + assert.True(t, m.CleanupDone) +} + +func TestJob_MainLoop_Panic(t *testing.T) { + m := &MockModule{ + CollectFunc: func() map[string]int64 { + panic("panic in Collect") + }, + } + job := newTestJob() + job.module = m + job.updateEvery = 1 + + go func() { + for i := 1; i < 3; i++ { + time.Sleep(time.Second) + job.Tick(i) + } + job.Stop() + }() + + job.Start() + + assert.True(t, job.Panicked()) + assert.True(t, m.CleanupDone) +} + +func TestJob_Tick(t *testing.T) { + job := newTestJob() + for i := 0; i < 3; i++ { + job.Tick(i) + } +} diff --git a/src/go/plugin/go.d/agent/module/mock.go b/src/go/plugin/go.d/agent/module/mock.go new file mode 100644 index 000000000..f83c7dbcc --- /dev/null +++ b/src/go/plugin/go.d/agent/module/mock.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import "errors" + +const MockConfigSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "option_str": { + "type": "string", + "description": "Option string value" + }, + "option_int": { + "type": "integer", + "description": "Option integer value" + } + }, + "required": [ + "option_str", + "option_int" + ] +} +` + +type MockConfiguration struct { + OptionStr string `yaml:"option_str" json:"option_str"` + OptionInt int `yaml:"option_int" json:"option_int"` +} + +// MockModule MockModule. +type MockModule struct { + Base + + Config MockConfiguration `yaml:",inline" json:""` + + FailOnInit bool + + InitFunc func() error + CheckFunc func() error + ChartsFunc func() *Charts + CollectFunc func() map[string]int64 + CleanupFunc func() + CleanupDone bool +} + +// Init invokes InitFunc. +func (m *MockModule) Init() error { + if m.FailOnInit { + return errors.New("mock init error") + } + if m.InitFunc == nil { + return nil + } + return m.InitFunc() +} + +// Check invokes CheckFunc. +func (m *MockModule) Check() error { + if m.CheckFunc == nil { + return nil + } + return m.CheckFunc() +} + +// Charts invokes ChartsFunc. +func (m *MockModule) Charts() *Charts { + if m.ChartsFunc == nil { + return nil + } + return m.ChartsFunc() +} + +// Collect invokes CollectDunc. +func (m *MockModule) Collect() map[string]int64 { + if m.CollectFunc == nil { + return nil + } + return m.CollectFunc() +} + +// Cleanup sets CleanupDone to true. +func (m *MockModule) Cleanup() { + if m.CleanupFunc != nil { + m.CleanupFunc() + } + m.CleanupDone = true +} + +func (m *MockModule) Configuration() any { + return m.Config +} diff --git a/src/go/plugin/go.d/agent/module/mock_test.go b/src/go/plugin/go.d/agent/module/mock_test.go new file mode 100644 index 000000000..d7521911f --- /dev/null +++ b/src/go/plugin/go.d/agent/module/mock_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMockModule_Init(t *testing.T) { + m := &MockModule{} + + assert.NoError(t, m.Init()) + m.InitFunc = func() error { return nil } + assert.NoError(t, m.Init()) +} + +func TestMockModule_Check(t *testing.T) { + m := &MockModule{} + + assert.NoError(t, m.Check()) + m.CheckFunc = func() error { return nil } + assert.NoError(t, m.Check()) +} + +func TestMockModule_Charts(t *testing.T) { + m := &MockModule{} + c := &Charts{} + + assert.Nil(t, m.Charts()) + m.ChartsFunc = func() *Charts { return c } + assert.True(t, c == m.Charts()) +} + +func TestMockModule_Collect(t *testing.T) { + m := &MockModule{} + d := map[string]int64{ + "1": 1, + } + + assert.Nil(t, m.Collect()) + m.CollectFunc = func() map[string]int64 { return d } + assert.Equal(t, d, m.Collect()) +} + +func TestMockModule_Cleanup(t *testing.T) { + m := &MockModule{} + require.False(t, m.CleanupDone) + + m.Cleanup() + assert.True(t, m.CleanupDone) +} diff --git a/src/go/plugin/go.d/agent/module/module.go b/src/go/plugin/go.d/agent/module/module.go new file mode 100644 index 000000000..13e20f2ae --- /dev/null +++ b/src/go/plugin/go.d/agent/module/module.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "encoding/json" + "testing" + + "github.com/netdata/netdata/go/plugins/logger" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +// Module is an interface that represents a module. +type Module interface { + // Init does initialization. + // If it returns error, the job will be disabled. + Init() error + + // Check is called after Init. + // If it returns error, the job will be disabled. + Check() error + + // Charts returns the chart definition. + Charts() *Charts + + // Collect collects metrics. + Collect() map[string]int64 + + // Cleanup Cleanup + Cleanup() + + GetBase() *Base + + Configuration() any +} + +// Base is a helper struct. All modules should embed this struct. +type Base struct { + *logger.Logger +} + +func (b *Base) GetBase() *Base { return b } + +func TestConfigurationSerialize(t *testing.T, mod Module, cfgJSON, cfgYAML []byte) { + t.Helper() + tests := map[string]struct { + config []byte + unmarshal func(in []byte, out interface{}) (err error) + marshal func(in interface{}) (out []byte, err error) + }{ + "json": {config: cfgJSON, marshal: json.Marshal, unmarshal: json.Unmarshal}, + "yaml": {config: cfgYAML, marshal: yaml.Marshal, unmarshal: yaml.Unmarshal}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + + require.NoError(t, test.unmarshal(test.config, mod), "unmarshal test->mod") + bs, err := test.marshal(mod.Configuration()) + require.NoError(t, err, "marshal mod config") + + var want map[string]any + var got map[string]any + + require.NoError(t, test.unmarshal(test.config, &want), "unmarshal test->map") + require.NoError(t, test.unmarshal(bs, &got), "unmarshal mod->map") + + require.NotNil(t, want, "want map") + require.NotNil(t, got, "got map") + + assert.Equal(t, want, got) + }) + } +} diff --git a/src/go/plugin/go.d/agent/module/registry.go b/src/go/plugin/go.d/agent/module/registry.go new file mode 100644 index 000000000..1d2aa9477 --- /dev/null +++ b/src/go/plugin/go.d/agent/module/registry.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import "fmt" + +const ( + UpdateEvery = 1 + AutoDetectionRetry = 0 + Priority = 70000 +) + +// Defaults is a set of module default parameters. +type Defaults struct { + UpdateEvery int + AutoDetectionRetry int + Priority int + Disabled bool +} + +type ( + // Creator is a Job builder. + Creator struct { + Defaults + Create func() Module + JobConfigSchema string + Config func() any + } + // Registry is a collection of Creators. + Registry map[string]Creator +) + +// DefaultRegistry DefaultRegistry. +var DefaultRegistry = Registry{} + +// Register registers a module in the DefaultRegistry. +func Register(name string, creator Creator) { + DefaultRegistry.Register(name, creator) +} + +// Register registers a module. +func (r Registry) Register(name string, creator Creator) { + if _, ok := r[name]; ok { + panic(fmt.Sprintf("%s is already in registry", name)) + } + r[name] = creator +} + +func (r Registry) Lookup(name string) (Creator, bool) { + v, ok := r[name] + return v, ok +} diff --git a/src/go/plugin/go.d/agent/module/registry_test.go b/src/go/plugin/go.d/agent/module/registry_test.go new file mode 100644 index 000000000..c9f31105a --- /dev/null +++ b/src/go/plugin/go.d/agent/module/registry_test.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package module + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRegister(t *testing.T) { + modName := "modName" + registry := make(Registry) + + // OK case + assert.NotPanics( + t, + func() { + registry.Register(modName, Creator{}) + }) + + _, exist := registry[modName] + + require.True(t, exist) + + // Panic case + assert.Panics( + t, + func() { + registry.Register(modName, Creator{}) + }) + +} |