@cryptotaxi247 / netdata-1 / commits / 31e8ac86e

chore(go.d): add type=float dimension support (#21362)

Ilya Mashchenko committed Nov 27, 2025 at 20:03 UTC 31e8ac86e0f45f449da840f7286847304c677eac
6 files changed +95 -39
src/go/pkg/netdataapi/api.go
+9 -2
@@ -87,14 +87,21 @@ func (a *API) SET(id string, value int64) {
87 _, _ = a.Write([]byte("SET '" + id + "' = " + strconv.FormatInt(value, 10) + "\n"))
88 }
89
90 +// SETFLOAT sets the value of a dimension for the initialized chart.
91 +func (a *API) SETFLOAT(id string, value float64) {
92 + v := strconv.FormatFloat(value, 'f', -1, 64)
93 + _, _ = a.Write([]byte("SET '" + id + "' = " + v + "\n"))
94 +}
95 +
96 // SETEMPTY sets an empty value for a dimension in the initialized chart.
97 func (a *API) SETEMPTY(id string) {
98 _, _ = a.Write([]byte("SET '" + id + "' = \n"))
99 }
100
101 // VARIABLE sets the value of a CHART scope variable for the initialized chart.
96 -func (a *API) VARIABLE(ID string, value int64) {
97 - _, _ = a.Write([]byte("VARIABLE CHART '" + ID + "' = " + strconv.FormatInt(value, 10) + "\n"))
102 +func (a *API) VARIABLE(ID string, value float64) {
103 + v := strconv.FormatFloat(value, 'f', -1, 64)
104 + _, _ = a.Write([]byte("VARIABLE CHART '" + ID + "' = " + v + "\n"))
105 }
106
107 // END completes data collection for the initialized chart.
src/go/pkg/netdataapi/api_test.go
+13 -1
@@ -135,6 +135,17 @@ func TestSET(t *testing.T) {
135 require.Equal(t, expected, w.String())
136 }
137
138 +func TestSETFLOAT(t *testing.T) {
139 + w := &bytes.Buffer{}
140 + api := New(w)
141 +
142 + api.SETFLOAT("cpu_user", 42.42)
143 +
144 + expected := "SET 'cpu_user' = 42.42\n"
145 +
146 + require.Equal(t, expected, w.String())
147 +}
148 +
149 func TestSETEMPTY(t *testing.T) {
150 w := &bytes.Buffer{}
151 api := New(w)
@@ -151,8 +162,9 @@ func TestVARIABLE(t *testing.T) {
162 api := New(w)
163
164 api.VARIABLE("var1", 100)
165 + api.VARIABLE("var2", 100.1)
166
155 - expected := "VARIABLE CHART 'var1' = 100\n"
167 + expected := "VARIABLE CHART 'var1' = 100\nVARIABLE CHART 'var2' = 100.1\n"
168
169 require.Equal(t, expected, w.String())
170 }
src/go/plugin/go.d/agent/module/charts.go
+5 -1
@@ -127,6 +127,7 @@ type (
127 Hidden bool
128 NoReset bool
129 NoOverflow bool
130 + Float bool
131 }
132
133 // Dim represents a chart dimension.
@@ -147,7 +148,7 @@ type (
148 Var struct {
149 ID string
150 Name string
150 - Value int64
151 + Value float64
152 }
153
154 // Dims is a collection of dims.
@@ -191,6 +192,9 @@ func (o DimOpts) String() string {
192 if o.Obsolete {
193 b.WriteString(" obsolete")
194 }
195 + if o.Float {
196 + b.WriteString(" type=float")
197 + }
198
199 if len(b.String()) == 0 {
200 return ""
src/go/plugin/go.d/agent/module/charts_test.go
+2 -2
@@ -88,8 +88,8 @@ func TestDimOpts_String(t *testing.T) {
88 }{
89 {"", DimOpts{}},
90 {
91 - "hidden nooverflow noreset obsolete",
92 - DimOpts{Hidden: true, NoOverflow: true, NoReset: true, Obsolete: true},
91 + "hidden nooverflow noreset obsolete type=float",
92 + DimOpts{Hidden: true, NoOverflow: true, NoReset: true, Obsolete: true, Float: true},
93 },
94 {
95 "hidden obsolete",
src/go/plugin/go.d/agent/module/job.go
+62 -33
@@ -177,6 +177,28 @@ type Job struct {
177 dumpAnalyzer interface{} // Will be *agent.DumpAnalyzer but avoid circular dependency
178 }
179
180 +type collectedMetrics struct {
181 + intMetrics map[string]int64
182 + floatMetrics map[string]float64
183 +}
184 +
185 +func (cm *collectedMetrics) getDimValue(dim *Dim) (float64, bool) {
186 + if dim.Float {
187 + v, ok := cm.floatMetrics[dim.ID]
188 + return v, ok
189 + }
190 + v, ok := cm.intMetrics[dim.ID]
191 + return float64(v), ok
192 +}
193 +
194 +func (cm *collectedMetrics) getVarValue(vr *Var) (float64, bool) {
195 + if v, ok := cm.floatMetrics[vr.ID]; ok {
196 + return v, ok
197 + }
198 + v, ok := cm.intMetrics[vr.ID]
199 + return float64(v), ok
200 +}
201 +
202 // NetdataChartIDMaxLength is the chart ID max length. See RRD_ID_LENGTH_MAX in the netdata source code.
203 const NetdataChartIDMaxLength = 1200
204
@@ -424,7 +446,7 @@ func (j *Job) runOnce() {
446 j.buf.Reset()
447 }
448
427 -func (j *Job) collect() (result map[string]int64) {
449 +func (j *Job) collect() collectedMetrics {
450 j.panicked = false
451 defer func() {
452 if r := recover(); r != nil {
@@ -435,21 +457,29 @@ func (j *Job) collect() (result map[string]int64) {
457 }
458 }
459 }()
438 - result = j.module.Collect(context.TODO())
460 +
461 + var mx collectedMetrics
462 +
463 + if v, ok := j.module.(MetricCollector); ok {
464 + mx.floatMetrics = v.CollectMetrics(context.TODO())
465 + } else {
466 + mx.intMetrics = j.module.Collect(context.TODO())
467 + }
468
469 // Record collected metrics for dump mode
441 - if j.dumpMode && j.dumpAnalyzer != nil && result != nil {
470 + // TODO: The dump analyzer only records intMetrics but ignores floatMetrics
471 + if j.dumpMode && j.dumpAnalyzer != nil && mx.intMetrics != nil {
472 if analyzer, ok := j.dumpAnalyzer.(interface {
473 RecordCollection(string, map[string]int64)
474 }); ok {
445 - analyzer.RecordCollection(j.name, result)
475 + analyzer.RecordCollection(j.name, mx.intMetrics)
476 }
477 }
478
449 - return result
479 + return mx
480 }
481
452 -func (j *Job) processMetrics(metrics map[string]int64, startTime time.Time, sinceLastRun int) bool {
482 +func (j *Job) processMetrics(mx collectedMetrics, startTime time.Time, sinceLastRun int) bool {
483 var createChart bool
484 if j.module.VirtualNode() == nil {
485 select {
@@ -495,10 +525,10 @@ func (j *Job) processMetrics(metrics map[string]int64, startTime time.Time, sinc
525 }
526 (*j.charts)[i] = chart
527 i++
498 - if len(metrics) == 0 || chart.Obsolete {
528 + if len(mx.intMetrics)+len(mx.floatMetrics) == 0 || chart.Obsolete {
529 continue
530 }
501 - if j.updateChart(chart, metrics, sinceLastRun) {
531 + if j.updateChart(chart, mx, sinceLastRun) {
532 updated++
533 }
534 }
@@ -529,17 +559,15 @@ func (j *Job) processMetrics(metrics map[string]int64, startTime time.Time, sinc
559 }
560 }
561
532 - j.updateChart(
533 - j.collectStatusChart,
534 - map[string]int64{"success": metrix.Bool(updated > 0), "failed": metrix.Bool(updated == 0)},
535 - sinceLastRun,
536 - )
562 + intMx := collectedMetrics{intMetrics: map[string]int64{"success": metrix.Bool(updated > 0), "failed": metrix.Bool(updated == 0)}}
563 + j.updateChart(j.collectStatusChart, intMx, sinceLastRun)
564
565 if updated == 0 {
566 return false
567 }
568
542 - j.updateChart(j.collectDurationChart, map[string]int64{"duration": elapsed}, sinceLastRun)
569 + intMx = collectedMetrics{intMetrics: map[string]int64{"duration": elapsed}}
570 + j.updateChart(j.collectDurationChart, intMx, sinceLastRun)
571
572 return true
573 }
@@ -630,16 +658,13 @@ func (j *Job) createChart(chart *Chart) {
658 })
659 }
660 for _, v := range chart.Vars {
633 - if v.Name != "" {
634 - j.api.VARIABLE(v.Name, v.Value)
635 - } else {
636 - j.api.VARIABLE(v.ID, v.Value)
637 - }
661 + name := firstNotEmpty(v.Name, v.ID)
662 + j.api.VARIABLE(name, v.Value)
663 }
664 _ = j.api.EMPTYLINE()
665 }
666
642 -func (j *Job) updateChart(chart *Chart, collected map[string]int64, sinceLastRun int) bool {
667 +func (j *Job) updateChart(chart *Chart, mx collectedMetrics, sinceLastRun int) bool {
668 if chart.ignore {
669 dims := chart.Dims[:0]
670 for _, dim := range chart.Dims {
@@ -658,8 +683,7 @@ func (j *Job) updateChart(chart *Chart, collected map[string]int64, sinceLastRun
683 if dim.remove {
684 continue
685 }
661 - if _, ok := collected[dim.ID]; ok {
662 - hasData = true
686 + if _, hasData = mx.getDimValue(dim); hasData {
687 break
688 }
689 }
@@ -682,25 +706,30 @@ func (j *Job) updateChart(chart *Chart, collected map[string]int64, sinceLastRun
706 }
707 chart.Dims[i] = dim
708 i++
685 - if v, ok := collected[dim.ID]; !ok {
686 - j.api.SETEMPTY(firstNotEmpty(dim.Name, dim.ID))
709 +
710 + name := firstNotEmpty(dim.Name, dim.ID)
711 + v, ok := mx.getDimValue(dim)
712 + if !ok {
713 + j.api.SETEMPTY(name)
714 + continue
715 + }
716 + updated++
717 + if dim.Float {
718 + j.api.SETFLOAT(name, v)
719 } else {
688 - j.api.SET(firstNotEmpty(dim.Name, dim.ID), v)
689 - updated++
720 + j.api.SET(name, int64(v))
721 }
722 }
723 +
724 chart.Dims = chart.Dims[:i]
725
726 for _, vr := range chart.Vars {
695 - if v, ok := collected[vr.ID]; ok {
696 - if vr.Name != "" {
697 - j.api.VARIABLE(vr.Name, v)
698 - } else {
699 - j.api.VARIABLE(vr.ID, v)
700 - }
727 + if v, ok := mx.getVarValue(vr); ok {
728 + name := firstNotEmpty(vr.Name, vr.ID)
729 + j.api.VARIABLE(name, v)
730 }
702 -
731 }
732 +
733 j.api.END()
734
735 if chart.updated = updated > 0; chart.updated {
src/go/plugin/go.d/agent/module/module.go
+4
@@ -41,6 +41,10 @@ type Module interface {
41 VirtualNode() *vnodes.VirtualNode
42 }
43
44 +type MetricCollector interface {
45 + CollectMetrics(context.Context) map[string]float64
46 +}
47 +
48 // Base is a helper struct. All modules should embed this struct.
49 type Base struct {
50 *logger.Logger