master
go 262 lines 7.45 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartemit
4
5 import (
6 "fmt"
7 "math"
8 "sort"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
12 )
13
14 const (
15 // Netdata wire protocol label sources (CLABEL third argument).
16 labelSourceAuto = 1
17 labelSourceConf = 2
18 collectJobReservedLabel = "_collect_job"
19 maxTypeIDLen = 1200
20 )
21
22 type normalizedActions struct {
23 createCharts map[string]CreateChartAction
24 createDimsByID map[string][]CreateDimensionAction
25 updateCharts []UpdateChartAction
26 removeDimensions []RemoveDimensionAction
27 removeCharts []RemoveChartAction
28 }
29
30 type dimensionEmission struct {
31 Name string
32 Hidden bool
33 Float bool
34 Algorithm string
35 Multiplier int
36 Divisor int
37 Obsolete bool
38 }
39
40 // ApplyPlan emits chartengine actions to the Netdata wire API.
41 func ApplyPlan(api *netdataapi.API, plan Plan, env EmitEnv) error {
42 if api == nil {
43 return fmt.Errorf("chartemit: nil netdata api")
44 }
45 if strings.TrimSpace(env.TypeID) == "" {
46 return fmt.Errorf("chartemit: emit env type_id is required")
47 }
48 if env.UpdateEvery <= 0 {
49 env.UpdateEvery = 1
50 }
51 normalized := normalizeActions(plan.Actions)
52 if err := validateTypeIDBudget(env.TypeID, normalized); err != nil {
53 return err
54 }
55 if !hasEmissions(normalized) {
56 return nil
57 }
58 if err := emitHostSelection(api, env); err != nil {
59 return err
60 }
61 emitCreatePhase(api, env, normalized)
62 emitUpdatePhase(api, env, normalized.updateCharts)
63 emitRemovePhase(api, env, normalized)
64 return nil
65 }
66
67 func hasEmissions(actions normalizedActions) bool {
68 return len(actions.createCharts) > 0 ||
69 len(actions.createDimsByID) > 0 ||
70 len(actions.updateCharts) > 0 ||
71 len(actions.removeDimensions) > 0 ||
72 len(actions.removeCharts) > 0
73 }
74
75 func emitHostSelection(api *netdataapi.API, env EmitEnv) error {
76 if env.HostScope == nil {
77 api.HOST("")
78 return nil
79 }
80
81 guid := strings.TrimSpace(env.HostScope.GUID)
82 if guid == "" {
83 return fmt.Errorf("chartemit: emit env host scope guid is required")
84 }
85 if sanitizeWireID(guid) != guid {
86 return fmt.Errorf("chartemit: emit env host scope guid contains unsupported characters")
87 }
88 if env.HostScope.Define != nil {
89 define, err := PrepareHostInfo(*env.HostScope.Define)
90 if err != nil {
91 return err
92 }
93 if define.GUID != guid {
94 return fmt.Errorf("chartemit: host define guid %q does not match host scope guid %q", env.HostScope.Define.GUID, env.HostScope.GUID)
95 }
96 api.HOSTINFO(define)
97 }
98 api.HOST(guid)
99 return nil
100 }
101
102 func validateTypeIDBudget(typeID string, actions normalizedActions) error {
103 typeID = sanitizeWireID(typeID)
104 if typeID == "" {
105 return fmt.Errorf("chartemit: emit env type_id is required")
106 }
107 seen := make(map[string]struct{})
108 for chartID := range actions.createCharts {
109 seen[chartID] = struct{}{}
110 }
111 for chartID := range actions.createDimsByID {
112 seen[chartID] = struct{}{}
113 }
114 for _, update := range actions.updateCharts {
115 seen[update.ChartID] = struct{}{}
116 }
117 for _, removeDim := range actions.removeDimensions {
118 seen[removeDim.ChartID] = struct{}{}
119 }
120 for _, removeChart := range actions.removeCharts {
121 seen[removeChart.ChartID] = struct{}{}
122 }
123 for chartID := range seen {
124 id := sanitizeWireID(chartID)
125 if len(typeID)+1+len(id) > maxTypeIDLen {
126 return fmt.Errorf("chartemit: type.id exceeds max length (%d): %s.%s", maxTypeIDLen, typeID, id)
127 }
128 }
129 return nil
130 }
131
132 func normalizeActions(actions []EngineAction) normalizedActions {
133 out := normalizedActions{
134 createCharts: make(map[string]CreateChartAction),
135 createDimsByID: make(map[string][]CreateDimensionAction),
136 }
137 for _, action := range actions {
138 switch v := action.(type) {
139 case CreateChartAction:
140 out.createCharts[v.ChartID] = v
141 case CreateDimensionAction:
142 out.createDimsByID[v.ChartID] = append(out.createDimsByID[v.ChartID], v)
143 case UpdateChartAction:
144 out.updateCharts = append(out.updateCharts, v)
145 case RemoveDimensionAction:
146 out.removeDimensions = append(out.removeDimensions, v)
147 case RemoveChartAction:
148 out.removeCharts = append(out.removeCharts, v)
149 }
150 }
151 return out
152 }
153
154 func emitCreatePhase(api *netdataapi.API, env EmitEnv, actions normalizedActions) {
155 createdChartIDs := make([]string, 0, len(actions.createCharts))
156 for chartID := range actions.createCharts {
157 createdChartIDs = append(createdChartIDs, chartID)
158 }
159 sort.Strings(createdChartIDs)
160 for _, chartID := range createdChartIDs {
161 createChart := actions.createCharts[chartID]
162 emitChart(api, env, createChart.ChartID, createChart.Meta, false)
163 emitChartLabels(api, env, createChart.Labels)
164 api.CLABELCOMMIT()
165 dims := actions.createDimsByID[chartID]
166 for _, dim := range dims {
167 emitDimension(api, dimensionEmission{
168 Name: dim.Name,
169 Hidden: dim.Hidden,
170 Float: dim.Float,
171 Algorithm: string(dim.Algorithm),
172 Multiplier: dim.Multiplier,
173 Divisor: dim.Divisor,
174 })
175 }
176 // Mark dimensions as emitted alongside explicit chart-create path.
177 delete(actions.createDimsByID, chartID)
178 }
179
180 remainingChartIDs := make([]string, 0, len(actions.createDimsByID))
181 for chartID := range actions.createDimsByID {
182 remainingChartIDs = append(remainingChartIDs, chartID)
183 }
184 sort.Strings(remainingChartIDs)
185 for _, chartID := range remainingChartIDs {
186 dims := actions.createDimsByID[chartID]
187 if len(dims) == 0 {
188 continue
189 }
190 emitChart(api, env, chartID, dims[0].ChartMeta, false)
191 // Dimension-only chart creation path still needs chart labels and commit.
192 emitChartLabels(api, env, nil)
193 api.CLABELCOMMIT()
194 for _, dim := range dims {
195 emitDimension(api, dimensionEmission{
196 Name: dim.Name,
197 Hidden: dim.Hidden,
198 Float: dim.Float,
199 Algorithm: string(dim.Algorithm),
200 Multiplier: dim.Multiplier,
201 Divisor: dim.Divisor,
202 })
203 }
204 }
205 }
206
207 func emitUpdatePhase(api *netdataapi.API, env EmitEnv, updates []UpdateChartAction) {
208 for _, update := range updates {
209 api.BEGIN(sanitizeWireID(env.TypeID), sanitizeWireID(update.ChartID), env.MSSinceLast)
210 for _, dim := range update.Values {
211 if dim.IsEmpty {
212 api.SETEMPTY(sanitizeWireID(dim.Name))
213 continue
214 }
215 if dim.IsFloat {
216 // Defensive: a non-finite float renders as 0 on the wire (the C parser accepts
217 // only lowercase "nan"); emit a gap. The planner already maps these to IsEmpty.
218 if math.IsNaN(dim.Float64) || math.IsInf(dim.Float64, 0) {
219 api.SETEMPTY(sanitizeWireID(dim.Name))
220 continue
221 }
222 api.SETFLOAT(sanitizeWireID(dim.Name), dim.Float64)
223 continue
224 }
225 api.SET(sanitizeWireID(dim.Name), dim.Int64)
226 }
227 api.END()
228 }
229 }
230
231 func emitRemovePhase(api *netdataapi.API, env EmitEnv, actions normalizedActions) {
232 for _, removeDim := range actions.removeDimensions {
233 emitChart(api, env, removeDim.ChartID, removeDim.ChartMeta, false)
234 emitDimension(api, dimensionEmission{
235 Name: removeDim.Name,
236 Hidden: removeDim.Hidden,
237 Float: removeDim.Float,
238 Algorithm: string(removeDim.Algorithm),
239 Multiplier: removeDim.Multiplier,
240 Divisor: removeDim.Divisor,
241 Obsolete: true,
242 })
243 }
244 for _, removeChart := range actions.removeCharts {
245 emitChart(api, env, removeChart.ChartID, removeChart.Meta, true)
246 }
247 }
248
249 func emitDimension(api *netdataapi.API, dim dimensionEmission) {
250 name := sanitizeWireID(dim.Name)
251 if name == "" {
252 return
253 }
254 api.DIMENSION(netdataapi.DimensionOpts{
255 ID: name,
256 Name: name,
257 Algorithm: dim.Algorithm,
258 Multiplier: handleZero(dim.Multiplier),
259 Divisor: handleZero(dim.Divisor),
260 Options: makeDimensionOptions(dim.Hidden, dim.Obsolete, dim.Float),
261 })
262 }