@cryptotaxi247 / netdata-1 / commits / 654770c71

feat(go.d/framework): add float dimension option handling (#21825)

Ilya Mashchenko committed Feb 26, 2026 at 12:47 UTC 654770c71b7b97a10732009e5b6e66c3d8e9b414
23 files changed +220 -16
src/go/pkg/metrix/README.md
+1 -1
@@ -81,7 +81,7 @@
81 | `WithSummaryReservoirSize(...)` | Stateful summary estimator size |
82 | `WithStateSetStates(...)` | StateSet allowed states |
83 | `WithStateSetMode(...)` | `ModeBitSet` (multiple simultaneous active states) or `ModeEnum` (exactly one active state) |
84 -| `WithDescription(...)`, `WithChartFamily(...)`, `WithUnit(...)` | Metric metadata hints for downstream consumers (e.g., autogen) |
84 +| `WithDescription(...)`, `WithChartFamily(...)`, `WithUnit(...)`, `WithFloat(...)` | Metric metadata hints for downstream consumers (e.g., autogen chart identity + float SET mode) |
85
86 ## Read Modes
87
src/go/pkg/metrix/collector_store.go
+4
@@ -499,6 +499,7 @@ func (c *storeCore) registerInstrument(name string, kind metricKind, mode metric
499 Description: strings.TrimSpace(cfg.description),
500 ChartFamily: strings.TrimSpace(cfg.chartFamily),
501 Unit: strings.TrimSpace(cfg.unit),
502 + Float: cfg.float,
503 }
504
505 var histogram *histogramSchema
@@ -564,6 +565,9 @@ func (c *storeCore) registerInstrument(name string, kind metricKind, mode metric
565 if cfg.unitSet && d.meta.Unit != metricMeta.Unit {
566 return nil, fmt.Errorf("metrix: metric unit mismatch for %s", name)
567 }
568 + if cfg.floatSet && d.meta.Float != metricMeta.Float {
569 + return nil, fmt.Errorf("metrix: metric float mismatch for %s", name)
570 + }
571 return d, nil
572 }
573
src/go/pkg/metrix/metric_meta_store_test.go
+18
@@ -22,6 +22,7 @@ func TestMetricMetaScenarios(t *testing.T) {
22 WithDescription("Busy workers"),
23 WithChartFamily("Workers"),
24 WithUnit("workers"),
25 + WithFloat(true),
26 )
27
28 cc.BeginCycle()
@@ -33,6 +34,7 @@ func TestMetricMetaScenarios(t *testing.T) {
34 assert.Equal(t, "Busy workers", meta.Description)
35 assert.Equal(t, "Workers", meta.ChartFamily)
36 assert.Equal(t, "workers", meta.Unit)
37 + assert.True(t, meta.Float)
38 },
39 },
40 "unknown metric metadata is unavailable": {
@@ -53,6 +55,7 @@ func TestMetricMetaScenarios(t *testing.T) {
55 WithDescription("Latency"),
56 WithChartFamily("Service"),
57 WithUnit("ms"),
58 + WithFloat(true),
59 )
60
61 cc.BeginCycle()
@@ -76,18 +79,21 @@ func TestMetricMetaScenarios(t *testing.T) {
79 assert.Equal(t, "Latency", meta.Description)
80 assert.Equal(t, "Service", meta.ChartFamily)
81 assert.Equal(t, "ms", meta.Unit)
82 + assert.True(t, meta.Float)
83
84 meta, ok = flat.MetricMeta("svc.latency_count")
85 require.True(t, ok, "expected flattened histogram count metric metadata")
86 assert.Equal(t, "Latency", meta.Description)
87 assert.Equal(t, "Service", meta.ChartFamily)
88 assert.Equal(t, "ms", meta.Unit)
89 + assert.True(t, meta.Float)
90
91 meta, ok = flat.MetricMeta("svc.latency_sum")
92 require.True(t, ok, "expected flattened histogram sum metric metadata")
93 assert.Equal(t, "Latency", meta.Description)
94 assert.Equal(t, "Service", meta.ChartFamily)
95 assert.Equal(t, "ms", meta.Unit)
96 + assert.True(t, meta.Float)
97 },
98 },
99 "metadata redeclaration conflict panics": {
@@ -100,6 +106,16 @@ func TestMetricMetaScenarios(t *testing.T) {
106 })
107 },
108 },
109 + "float metadata redeclaration conflict panics": {
110 + run: func(t *testing.T) {
111 + s := NewCollectorStore()
112 + w := s.Write().SnapshotMeter("apache")
113 + _ = w.Gauge("workers_busy", WithFloat(true))
114 + expectPanic(t, func() {
115 + _ = w.Gauge("workers_busy", WithFloat(false))
116 + })
117 + },
118 + },
119 "redeclare without metadata options keeps first metadata": {
120 run: func(t *testing.T) {
121 s := NewCollectorStore()
@@ -109,6 +125,7 @@ func TestMetricMetaScenarios(t *testing.T) {
125 WithDescription("Busy workers"),
126 WithChartFamily("Workers"),
127 WithUnit("workers"),
128 + WithFloat(true),
129 )
130 _ = w.Gauge("workers_busy")
131
@@ -122,6 +139,7 @@ func TestMetricMetaScenarios(t *testing.T) {
139 assert.Equal(t, "Busy workers", meta.Description)
140 assert.Equal(t, "Workers", meta.ChartFamily)
141 assert.Equal(t, "workers", meta.Unit)
142 + assert.True(t, meta.Float)
143 },
144 },
145 }
src/go/pkg/metrix/options.go
+10
@@ -30,6 +30,8 @@ type instrumentConfig struct {
30 chartFamily string
31 unitSet bool
32 unit string
33 + floatSet bool
34 + float bool
35 }
36
37 func WithFreshness(policy FreshnessPolicy) InstrumentOption {
@@ -103,3 +105,11 @@ func WithUnit(unit string) InstrumentOption {
105 cfg.unit = unit
106 })
107 }
108 +
109 +// WithFloat sets optional metric-family float-dimension metadata hint.
110 +func WithFloat(isFloat bool) InstrumentOption {
111 + return optionFunc(func(cfg *instrumentConfig) {
112 + cfg.floatSet = true
113 + cfg.float = isFloat
114 + })
115 +}
src/go/pkg/metrix/types.go
+1
@@ -54,6 +54,7 @@ type MetricMeta struct {
54 Description string
55 ChartFamily string
56 Unit string
57 + Float bool
58 }
59
60 // MetricKind identifies the logical metric family type.
src/go/plugin/framework/chartemit/apply.go
+8 -6
@@ -29,6 +29,7 @@ type normalizedActions struct {
29 type dimensionEmission struct {
30 Name string
31 Hidden bool
32 + Float bool
33 Algorithm string
34 Multiplier int
35 Divisor int
@@ -124,6 +125,7 @@ func emitCreatePhase(api *netdataapi.API, env EmitEnv, actions normalizedActions
125 emitDimension(api, dimensionEmission{
126 Name: dim.Name,
127 Hidden: dim.Hidden,
128 + Float: dim.Float,
129 Algorithm: string(dim.Algorithm),
130 Multiplier: dim.Multiplier,
131 Divisor: dim.Divisor,
@@ -151,6 +153,7 @@ func emitCreatePhase(api *netdataapi.API, env EmitEnv, actions normalizedActions
153 emitDimension(api, dimensionEmission{
154 Name: dim.Name,
155 Hidden: dim.Hidden,
156 + Float: dim.Float,
157 Algorithm: string(dim.Algorithm),
158 Multiplier: dim.Multiplier,
159 Divisor: dim.Divisor,
@@ -167,13 +170,11 @@ func emitUpdatePhase(api *netdataapi.API, env EmitEnv, updates []UpdateChartActi
170 api.SETEMPTY(sanitizeWireID(dim.Name))
171 continue
172 }
170 - value := dim.Int64
173 if dim.IsFloat {
172 - // NOTE: V2 currently emits integer SET only.
173 - // TODO(godv2): switch to SETFLOAT when Netdata float wire support is available.
174 - value = int64(dim.Float64)
174 + api.SETFLOAT(sanitizeWireID(dim.Name), dim.Float64)
175 + continue
176 }
176 - api.SET(sanitizeWireID(dim.Name), value)
177 + api.SET(sanitizeWireID(dim.Name), dim.Int64)
178 }
179 api.END()
180 }
@@ -185,6 +186,7 @@ func emitRemovePhase(api *netdataapi.API, env EmitEnv, actions normalizedActions
186 emitDimension(api, dimensionEmission{
187 Name: removeDim.Name,
188 Hidden: removeDim.Hidden,
189 + Float: removeDim.Float,
190 Algorithm: string(removeDim.Algorithm),
191 Multiplier: removeDim.Multiplier,
192 Divisor: removeDim.Divisor,
@@ -207,6 +209,6 @@ func emitDimension(api *netdataapi.API, dim dimensionEmission) {
209 Algorithm: dim.Algorithm,
210 Multiplier: handleZero(dim.Multiplier),
211 Divisor: handleZero(dim.Divisor),
210 - Options: makeDimensionOptions(dim.Hidden, dim.Obsolete),
212 + Options: makeDimensionOptions(dim.Hidden, dim.Obsolete, dim.Float),
213 })
214 }
src/go/plugin/framework/chartemit/apply_test.go
+64 -3
@@ -40,6 +40,7 @@ func TestApplyPlanEmitsNetdataWire(t *testing.T) {
40 ChartMeta: meta,
41 Name: "received",
42 Hidden: false,
43 + Float: true,
44 Algorithm: chartengine.AlgorithmIncremental,
45 Multiplier: 1,
46 Divisor: 1,
@@ -59,6 +60,7 @@ func TestApplyPlanEmitsNetdataWire(t *testing.T) {
60 ChartMeta: meta,
61 Name: "received",
62 Hidden: false,
63 + Float: true,
64 Algorithm: chartengine.AlgorithmIncremental,
65 Multiplier: 1,
66 Divisor: 1,
@@ -88,10 +90,10 @@ func TestApplyPlanEmitsNetdataWire(t *testing.T) {
90 assert.Contains(t, out, "CLABEL 'instance' 'localhost' '2'")
91 assert.Contains(t, out, "CLABEL '_collect_job' 'job01' '1'")
92 assert.Contains(t, out, "CLABEL_COMMIT")
91 - assert.Contains(t, out, "DIMENSION 'received' 'received' 'incremental' '1' '1' ''")
93 + assert.Contains(t, out, "DIMENSION 'received' 'received' 'incremental' '1' '1' 'type=float'")
94 assert.Contains(t, out, "BEGIN 'collector.job.win_nic_traffic_eth0' 100")
93 - assert.Contains(t, out, "SET 'received' = 123")
94 - assert.Contains(t, out, "DIMENSION 'received' 'received' 'incremental' '1' '1' 'obsolete'")
95 + assert.Contains(t, out, "SET 'received' = 123.5")
96 + assert.Contains(t, out, "DIMENSION 'received' 'received' 'incremental' '1' '1' 'obsolete type=float'")
97 assert.Contains(t, out, "obsolete")
98
99 createPos := strings.Index(out, "CHART 'collector.job.win_nic_traffic_eth0'")
@@ -167,6 +169,65 @@ func TestApplyPlanAutogenChartCreateUpdateRemove(t *testing.T) {
169 assert.Contains(t, out, "obsolete")
170 }
171
172 +func TestApplyPlanUsesIntegerSETForNonFloatUpdates(t *testing.T) {
173 + var buf bytes.Buffer
174 + api := netdataapi.New(&buf)
175 +
176 + meta := chartengine.ChartMeta{
177 + Title: "Runtime jobs",
178 + Family: "Runtime",
179 + Context: "runtime.jobs",
180 + Units: "jobs",
181 + Algorithm: chartengine.AlgorithmAbsolute,
182 + Type: chartengine.ChartTypeLine,
183 + }
184 +
185 + plan := Plan{
186 + Actions: []EngineAction{
187 + chartengine.CreateChartAction{
188 + ChartTemplateID: "g0c0",
189 + ChartID: "runtime_jobs",
190 + Meta: meta,
191 + },
192 + chartengine.CreateDimensionAction{
193 + ChartID: "runtime_jobs",
194 + ChartMeta: meta,
195 + Name: "total",
196 + Algorithm: chartengine.AlgorithmAbsolute,
197 + Multiplier: 1,
198 + Divisor: 1,
199 + },
200 + chartengine.UpdateChartAction{
201 + ChartID: "runtime_jobs",
202 + Values: []chartengine.UpdateDimensionValue{
203 + {
204 + Name: "total",
205 + IsFloat: false,
206 + Int64: 7,
207 + Float64: 7.9,
208 + },
209 + },
210 + },
211 + },
212 + }
213 +
214 + err := ApplyPlan(api, plan, EmitEnv{
215 + TypeID: "collector.job",
216 + UpdateEvery: 1,
217 + Plugin: "go.d.plugin",
218 + Module: "runtime",
219 + JobName: "job01",
220 + MSSinceLast: 1,
221 + })
222 + require.NoError(t, err)
223 +
224 + out := buf.String()
225 + assert.Contains(t, out, "DIMENSION 'total' 'total' 'absolute' '1' '1' ''")
226 + assert.Contains(t, out, "BEGIN 'collector.job.runtime_jobs' 1")
227 + assert.Contains(t, out, "SET 'total' = 7")
228 + assert.NotContains(t, out, "SET 'total' = 7.9")
229 +}
230 +
231 func TestApplyPlanDimensionOnlyCreateEmitsLabelsAndCommit(t *testing.T) {
232 var buf bytes.Buffer
233 api := netdataapi.New(&buf)
src/go/plugin/framework/chartemit/order.go
+4 -1
@@ -4,7 +4,7 @@ package chartemit
4
5 import "strings"
6
7 -func makeDimensionOptions(hidden, obsolete bool) string {
7 +func makeDimensionOptions(hidden, obsolete, float bool) string {
8 var parts []string
9 if hidden {
10 parts = append(parts, "hidden")
@@ -12,6 +12,9 @@ func makeDimensionOptions(hidden, obsolete bool) string {
12 if obsolete {
13 parts = append(parts, "obsolete")
14 }
15 + if float {
16 + parts = append(parts, "type=float")
17 + }
18 return strings.Join(parts, " ")
19 }
20
src/go/plugin/framework/chartengine/actions.go
+2
@@ -38,6 +38,7 @@ type CreateDimensionAction struct {
38 ChartMeta program.ChartMeta
39 Name string
40 Hidden bool
41 + Float bool
42 Algorithm program.Algorithm
43 Multiplier int
44 Divisor int
@@ -68,6 +69,7 @@ type RemoveDimensionAction struct {
69 ChartMeta program.ChartMeta
70 Name string
71 Hidden bool
72 + Float bool
73 Algorithm program.Algorithm
74 Multiplier int
75 Divisor int
src/go/plugin/framework/chartengine/autogen.go
+3
@@ -27,6 +27,7 @@ type autogenRoute struct {
27 family string
28 contextName string
29 staticDimension bool
30 + float bool
31 }
32
33 type autogenSourceBuilder func(
@@ -99,6 +100,7 @@ func (e *Engine) resolveAutogenRoute(
100 Hidden: false,
101 Multiplier: 1,
102 Divisor: 1,
103 + Float: route.float,
104 Static: route.staticDimension,
105 Inferred: false,
106 Autogen: true,
@@ -188,6 +190,7 @@ func applyAutogenMetricMeta(route autogenRoute, meta metrix.MetricMeta, seriesMe
190 route.units = normalizeAutogenUnitByAlgorithm(unit, route.algorithm)
191 route.chartType = chartTypeFromUnits(route.units)
192 }
193 + route.float = meta.Float
194 return route
195 }
196
src/go/plugin/framework/chartengine/compiler.go
+3
@@ -239,6 +239,7 @@ func compileDimension(dim charttpl.Dimension, visibleMetrics map[string]struct{}
239 Hidden: options.hidden,
240 Multiplier: options.multiplier,
241 Divisor: options.divisor,
242 + Float: options.float,
243 Dynamic: inferFromSeriesMeta || nameFromLabel != "",
244 },
245 selectorKeys: append([]string(nil), meta.ConstrainedLabelKeys...),
@@ -251,6 +252,7 @@ type compiledDimensionOptions struct {
252 hidden bool
253 multiplier int
254 divisor int
255 + float bool
256 }
257
258 func compileDimensionOptions(in *charttpl.DimensionOptions) compiledDimensionOptions {
@@ -262,6 +264,7 @@ func compileDimensionOptions(in *charttpl.DimensionOptions) compiledDimensionOpt
264 return out
265 }
266 out.hidden = in.Hidden
267 + out.float = in.Float
268 if in.Multiplier != 0 {
269 out.multiplier = in.Multiplier
270 }
src/go/plugin/framework/chartengine/compiler_test.go
+2
@@ -60,6 +60,7 @@ func TestCompileScenarios(t *testing.T) {
60 Name: "total",
61 Options: &charttpl.DimensionOptions{
62 Hidden: true,
63 + Float: true,
64 Multiplier: -8,
65 Divisor: 1000,
66 },
@@ -76,6 +77,7 @@ func TestCompileScenarios(t *testing.T) {
77 require.Len(t, charts, 1)
78 require.Len(t, charts[0].Dimensions, 1)
79 assert.True(t, charts[0].Dimensions[0].Hidden)
80 + assert.True(t, charts[0].Dimensions[0].Float)
81 assert.Equal(t, -8, charts[0].Dimensions[0].Multiplier)
82 assert.Equal(t, 1000, charts[0].Dimensions[0].Divisor)
83 },
src/go/plugin/framework/chartengine/internal/program/dimension.go
+2
@@ -49,6 +49,8 @@ type Dimension struct {
49 Multiplier int
50 // Divisor maps to DIMENSION divisor option.
51 Divisor int
52 + // Float maps to DIMENSION type=float option.
53 + Float bool
54 // Dynamic is compile-derived and true when rendering can fan out by labels.
55 Dynamic bool
56 }
src/go/plugin/framework/chartengine/lifecycle.go
+3
@@ -27,6 +27,7 @@ type materializedChartState struct {
27 type materializedDimensionState struct {
28 name string
29 hidden bool
30 + float bool
31 static bool
32 order int
33 algorithm program.Algorithm
@@ -77,6 +78,7 @@ func (c *materializedChartState) ensureDimension(name string, state dimensionSta
78 c.orderedDimsDirty = true
79 }
80 dim.hidden = state.hidden
81 + dim.float = state.float
82 dim.static = state.static
83 dim.order = state.order
84 dim.algorithm = state.algorithm
@@ -87,6 +89,7 @@ func (c *materializedChartState) ensureDimension(name string, state dimensionSta
89 dim = &materializedDimensionState{
90 name: name,
91 hidden: state.hidden,
92 + float: state.float,
93 static: state.static,
94 order: state.order,
95 algorithm: state.algorithm,
src/go/plugin/framework/chartengine/matcher.go
+2
@@ -22,6 +22,7 @@ type routeBinding struct {
22 Hidden bool
23 Multiplier int
24 Divisor int
25 + Float bool
26 Static bool
27 Inferred bool
28 Autogen bool
@@ -130,6 +131,7 @@ func (e *Engine) resolveSeriesRoutes(
131 Hidden: candidate.dimension.Hidden,
132 Multiplier: candidate.dimension.Multiplier,
133 Divisor: candidate.dimension.Divisor,
134 + Float: candidate.dimension.Float,
135 Static: !candidate.dimension.Dynamic,
136 Inferred: candidate.dimension.InferNameFromSeriesMeta,
137 Autogen: false,
src/go/plugin/framework/chartengine/planner.go
+8 -1
@@ -72,6 +72,7 @@ type InferredDimension struct {
72
73 type dimensionState struct {
74 hidden bool
75 + float bool
76 static bool
77 order int
78 algorithm program.Algorithm
@@ -500,6 +501,7 @@ func (ctx *planBuildContext) accumulateRoute(
501 entry.value = value
502 entry.dimensionState = dimensionState{
503 hidden: route.Hidden,
504 + float: route.Float,
505 static: route.Static,
506 order: route.DimensionIndex,
507 algorithm: route.Algorithm,
@@ -511,6 +513,9 @@ func (ctx *planBuildContext) accumulateRoute(
513 if entry.hidden != route.Hidden {
514 // First-observed hidden flag wins within one build; conflicting routes are ignored.
515 }
516 + if entry.float != route.Float {
517 + // First-observed float flag wins within one build; conflicting routes are ignored.
518 + }
519 entry.value += value
520 }
521
@@ -578,6 +583,7 @@ func (e *Engine) materializePlanCharts(ctx *planBuildContext) error {
583 ChartMeta: cs.meta,
584 Name: name,
585 Hidden: entry.hidden,
586 + Float: entry.float,
587 Algorithm: entry.algorithm,
588 Multiplier: entry.multiplier,
589 Divisor: entry.divisor,
@@ -593,7 +599,8 @@ func (e *Engine) materializePlanCharts(ctx *planBuildContext) error {
599 if ok && entry != nil && entry.seenSeq == cs.currentBuildSeq {
600 values = append(values, UpdateDimensionValue{
601 Name: name,
596 - IsFloat: true,
602 + IsFloat: entry.float,
603 + Int64: int64(entry.value),
604 Float64: entry.value,
605 })
606 continue
src/go/plugin/framework/chartengine/planner_lifecycle.go
+2
@@ -245,6 +245,7 @@ func enforceDimensionCaps(
245 ChartMeta: matChart.meta,
246 Name: name,
247 Hidden: dim.hidden,
248 + Float: dim.float,
249 Algorithm: dim.algorithm,
250 Multiplier: dim.multiplier,
251 Divisor: dim.divisor,
@@ -319,6 +320,7 @@ func collectExpiryRemovals(
320 ChartMeta: matChart.meta,
321 Name: name,
322 Hidden: dim.hidden,
323 + Float: dim.float,
324 Algorithm: dim.algorithm,
325 Multiplier: dim.multiplier,
326 Divisor: dim.divisor,
src/go/plugin/framework/chartengine/planner_test.go
+73 -1
@@ -1056,6 +1056,58 @@ groups:
1056 assert.Equal(t, "ms/s", sum.Meta.Units)
1057 }
1058
1059 +func TestBuildPlanAutogenUsesMetricFloatMetadataForScalar(t *testing.T) {
1060 + e, err := New(WithAutogenPolicy(AutogenPolicy{Enabled: true}))
1061 + require.NoError(t, err)
1062 +
1063 + yaml := `
1064 +version: v1
1065 +groups:
1066 + - family: Service
1067 + metrics:
1068 + - svc.requests_total
1069 + charts:
1070 + - title: Requests
1071 + context: requests
1072 + units: requests/s
1073 + dimensions:
1074 + - selector: svc.requests_total
1075 + name: total
1076 +`
1077 + require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1078 +
1079 + store := metrix.NewCollectorStore()
1080 + cc := mustCycleController(t, store)
1081 + unmatched := store.Write().SnapshotMeter("svc").Gauge(
1082 + "temperature_celsius",
1083 + metrix.WithFloat(true),
1084 + )
1085 +
1086 + cc.BeginCycle()
1087 + unmatched.Observe(10.5)
1088 + cc.CommitCycleSuccess()
1089 +
1090 + plan, err := e.BuildPlan(store.Read(metrix.ReadFlatten()))
1091 + require.NoError(t, err)
1092 +
1093 + var created *CreateDimensionAction
1094 + for _, action := range plan.Actions {
1095 + dim, ok := action.(CreateDimensionAction)
1096 + if !ok || dim.ChartID != "svc.temperature_celsius" {
1097 + continue
1098 + }
1099 + created = &dim
1100 + break
1101 + }
1102 + require.NotNil(t, created)
1103 + assert.True(t, created.Float)
1104 + update := findUpdateAction(plan)
1105 + require.NotNil(t, update)
1106 + require.Len(t, update.Values, 1)
1107 + assert.True(t, update.Values[0].IsFloat)
1108 + assert.Equal(t, float64(10.5), update.Values[0].Float64)
1109 +}
1110 +
1111 func TestBuildPlanAutogenUsesMetricMetadataForSummaryWithoutQuantiles(t *testing.T) {
1112 e, err := New(WithAutogenPolicy(AutogenPolicy{Enabled: true}))
1113 require.NoError(t, err)
@@ -1126,7 +1178,7 @@ groups:
1178 store := metrix.NewCollectorStore()
1179 cc := mustCycleController(t, store)
1180 sm := store.Write().SnapshotMeter("svc")
1129 - m := sm.Counter("requests_total")
1181 + m := sm.Counter("requests_total", metrix.WithFloat(true))
1182 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
1183
1184 cc.BeginCycle()
@@ -1140,6 +1192,22 @@ groups:
1192 require.NotNil(t, create)
1193 assert.Equal(t, "svc_requests", create.ChartID)
1194 assert.NotEqual(t, "svc.requests_total-method=GET", create.ChartID)
1195 + var createdDim *CreateDimensionAction
1196 + for _, action := range plan.Actions {
1197 + dim, ok := action.(CreateDimensionAction)
1198 + if !ok || dim.ChartID != "svc_requests" {
1199 + continue
1200 + }
1201 + createdDim = &dim
1202 + break
1203 + }
1204 + require.NotNil(t, createdDim)
1205 + assert.False(t, createdDim.Float)
1206 + update := findUpdateAction(plan)
1207 + require.NotNil(t, update)
1208 + require.Len(t, update.Values, 1)
1209 + assert.False(t, update.Values[0].IsFloat)
1210 + assert.Equal(t, int64(10), update.Values[0].Int64)
1211 }
1212
1213 func TestBuildPlanAutogenStrictOverflowDrop(t *testing.T) {
@@ -1517,10 +1585,12 @@ groups:
1585 name_from_label: mode
1586 options:
1587 hidden: true
1588 + float: true
1589 - selector: m_b
1590 name_from_label: mode
1591 options:
1592 hidden: false
1593 + float: false
1594 `
1595 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1596
@@ -1557,10 +1627,12 @@ groups:
1627 require.NotNil(t, created)
1628 assert.Equal(t, "total", created.Name)
1629 assert.True(t, created.Hidden)
1630 + assert.True(t, created.Float)
1631
1632 update := findUpdateAction(plan)
1633 require.NotNil(t, update)
1634 require.Len(t, update.Values, 1)
1635 + assert.True(t, update.Values[0].IsFloat)
1636 assert.Equal(t, "total", update.Values[0].Name)
1637 assert.Equal(t, float64(8), update.Values[0].Float64)
1638 }
src/go/plugin/framework/chartengine/runtime_metrics_test.go
+2 -2
@@ -191,8 +191,8 @@ func TestEngineRuntimeObservabilityScenarios(t *testing.T) {
191 require.Equal(t, "netdata_go_plugin_component_component_jobs", update.ChartID)
192 require.Len(t, update.Values, 1)
193 assert.Equal(t, "total", update.Values[0].Name)
194 - assert.True(t, update.Values[0].IsFloat)
195 - assert.Equal(t, float64(7), update.Values[0].Float64)
194 + assert.False(t, update.Values[0].IsFloat)
195 + assert.Equal(t, int64(7), update.Values[0].Int64)
196 },
197 },
198 "autogen planning uses runtime metric metadata": {
src/go/plugin/framework/charttpl/README.md
+1
@@ -94,6 +94,7 @@ When multiple series share the same instance identity labels, they appear as dim
94 | `options.multiplier` | int | no | DIM multiplier (`0` means default `1`) |
95 | `options.divisor` | int | no | DIM divisor (`0` means default `1`) |
96 | `options.hidden` | bool | no | Mark dimension hidden |
97 +| `options.float` | bool | no | Emit `type=float` and use `SETFLOAT` updates |
98
99 **Selector syntax**: A selector takes the form `metric_name` or `metric_name{label=value, ...}`.
100 The metric name prefix is required; label-only selectors like `{label=value}` are rejected.
src/go/plugin/framework/charttpl/config_schema.json
+3
@@ -269,6 +269,9 @@
269 },
270 "hidden": {
271 "type": "boolean"
272 + },
273 + "float": {
274 + "type": "boolean"
275 }
276 }
277 }
src/go/plugin/framework/charttpl/spec.go
+2 -1
@@ -94,9 +94,10 @@ type Dimension struct {
94 Options *DimensionOptions `yaml:"options,omitempty" json:"options,omitempty"`
95 }
96
97 -// DimensionOptions controls emitted DIMENSION options.
97 +// DimensionOptions controls DIMENSION options and update emission mode.
98 type DimensionOptions struct {
99 Multiplier int `yaml:"multiplier,omitempty" json:"multiplier,omitempty"`
100 Divisor int `yaml:"divisor,omitempty" json:"divisor,omitempty"`
101 Hidden bool `yaml:"hidden,omitempty" json:"hidden,omitempty"`
102 + Float bool `yaml:"float,omitempty" json:"float,omitempty"`
103 }
src/go/plugin/framework/charttpl/spec_test.go
+2
@@ -47,6 +47,7 @@ groups:
47 multiplier: -8
48 divisor: 1000
49 hidden: true
50 + float: true
51 `,
52 assert: func(t *testing.T, spec *Spec) {
53 t.Helper()
@@ -58,6 +59,7 @@ groups:
59 assert.Equal(t, -8, spec.Groups[0].Charts[0].Dimensions[0].Options.Multiplier)
60 assert.Equal(t, 1000, spec.Groups[0].Charts[0].Dimensions[0].Options.Divisor)
61 assert.True(t, spec.Groups[0].Charts[0].Dimensions[0].Options.Hidden)
62 + assert.True(t, spec.Groups[0].Charts[0].Dimensions[0].Options.Float)
63 require.NotNil(t, spec.Engine)
64 require.NotNil(t, spec.Engine.Selector)
65 assert.Equal(t, []string{`mysql_queries_total{db="main"}`}, spec.Engine.Selector.Allow)