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