master
go 423 lines 12.1 KB
Raw
1 package framework
2
3 import (
4 "fmt"
5 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
6 "reflect"
7 "strings"
8 "time"
9 )
10
11 // NewCollectorState creates a new collector state
12 func NewCollectorState() *CollectorState {
13 return &CollectorState{
14 instances: make(map[string]*Instance),
15 obsoleteInstances: make([]string, 0),
16 metrics: make([]MetricValue, 0, 1000),
17 errors: make(map[string]error),
18 protocols: make(map[string]*ProtocolMetrics),
19 }
20 }
21
22 // GetInstance returns or creates an instance for the given context and labels
23 func (s *CollectorState) GetInstance(ctx any, labels any) *Instance {
24 // Extract context metadata including label order
25 contextMeta := extractContextMetadata(ctx)
26 if contextMeta == nil {
27 return nil
28 }
29
30 // Extract context name
31 contextName := contextMeta.Name
32
33 // Generate instance key
34 var key string
35 var labelMap map[string]string
36
37 if labels != nil {
38 // Always convert to map for storage
39 labelMap = structToMap(labels)
40
41 // Use the InstanceID method if available
42 if labeler, ok := labels.(interface{ InstanceID(string) string }); ok {
43 key = labeler.InstanceID(contextName)
44 } else {
45 // Fallback to manual generation
46 key = generateInstanceKeyWithOrder(contextName, contextMeta.LabelOrder, labelMap)
47 }
48 } else {
49 // No labels, just use context name
50 key = contextName
51 labelMap = make(map[string]string)
52 }
53
54 // Get or create instance
55 instance, exists := s.instances[key]
56 if !exists {
57 instance = &Instance{
58 key: key,
59 contextName: contextName,
60 labels: labelMap,
61 lastSeen: time.Now(),
62 }
63 s.instances[key] = instance
64 }
65
66 instance.lastSeen = time.Now()
67 return instance
68 }
69
70 // SetMetricsForGeneratedCode is ONLY for use by code generated by metricgen.
71 // DO NOT call this method directly in hand-written code.
72 // Use the type-safe Set() methods on generated context types instead.
73 func (s *CollectorState) SetMetricsForGeneratedCode(ctx any, labels any, values map[string]int64) {
74 s.set(ctx, labels, values)
75 }
76
77 // SetUpdateEveryOverrideForGeneratedCode is ONLY for use by code generated by metricgen.
78 // DO NOT call this method directly in hand-written code.
79 // Use the type-safe SetUpdateEvery() methods on generated context types instead.
80 func (s *CollectorState) SetUpdateEveryOverrideForGeneratedCode(ctx any, labels any, updateEvery int) {
81 instance := s.GetInstance(ctx, labels)
82 if instance != nil {
83 instance.UpdateEveryOverride = updateEvery
84 }
85 }
86
87 // set stores multiple metric values for an instance
88 // This method is unexported to prevent direct usage by modules.
89 // Use the type-safe Set() methods on generated context types instead.
90 func (s *CollectorState) set(ctx any, labels any, values map[string]int64) {
91 instance := s.GetInstance(ctx, labels)
92
93 // Get context metadata
94 contextMeta := getContextMetadata(ctx)
95
96 // Store each metric value
97 for dimName, rawValue := range values {
98 // Find dimension metadata
99 var dim *Dimension
100 for _, d := range contextMeta.Dimensions {
101 if d.Name == dimName {
102 dim = &d
103 break
104 }
105 }
106
107 if dim == nil {
108 // Log warning for unknown dimensions to help catch typos and configuration errors
109 s.Warningf("unknown dimension '%s' for context '%s' - dimension will be skipped", dimName, contextMeta.Name)
110 continue
111 }
112
113 // Apply precision FIRST, then unit conversion to avoid integer division precision loss
114 // 1. First apply precision multiplication to preserve accuracy
115 // 2. Then apply unit conversion: (value * mul) / div
116 // This ensures accurate conversion to base units
117 precisionValue := rawValue * int64(dim.Precision)
118 finalValue := precisionValue
119 if dim.Mul != 0 && dim.Div != 0 {
120 finalValue = (precisionValue * int64(dim.Mul)) / int64(dim.Div)
121 }
122
123 s.metrics = append(s.metrics, MetricValue{
124 Instance: *instance,
125 Dimension: dimName,
126 Value: finalValue,
127 Timestamp: time.Now(),
128 })
129 }
130 }
131
132 // IsTimeFor checks if it's time to collect a specific context
133 func (s *CollectorState) IsTimeFor(contextName string) bool {
134 // Get context metadata to find update interval
135 interval := getContextUpdateInterval(contextName)
136 if interval <= 1 {
137 return true // Collect every iteration
138 }
139
140 // Check if current iteration is a multiple of the interval
141 return s.iteration%int64(interval) == 0
142 }
143
144 // TrackError records an error for a specific object
145 func (s *CollectorState) TrackError(objectType, objectName string, err error) {
146 key := fmt.Sprintf("%s:%s", objectType, objectName)
147 s.errors[key] = err
148 }
149
150 // ClearErrors removes all tracked errors
151 func (s *CollectorState) ClearErrors() {
152 s.errors = make(map[string]error)
153 }
154
155 // NextIteration handles obsoletion and clears metrics for next iteration
156 func (s *CollectorState) NextIteration(obsoletionTimeout int) {
157 // Note: iteration counter is now incremented in Collect()
158
159 // Clear metrics for next iteration
160 s.metrics = s.metrics[:0]
161
162 // Clear previous obsolete instances list
163 s.obsoleteInstances = s.obsoleteInstances[:0]
164
165 // Check for obsolete instances
166 cutoff := time.Now().Add(-time.Duration(obsoletionTimeout) * time.Second)
167 for key, instance := range s.instances {
168 if instance.lastSeen.Before(cutoff) {
169 // Track as obsolete for chart cleanup
170 s.obsoleteInstances = append(s.obsoleteInstances, key)
171 delete(s.instances, key)
172 }
173 }
174 }
175
176 // GetMetrics returns all collected metrics for the current iteration
177 func (s *CollectorState) GetMetrics() []MetricValue {
178 return s.metrics
179 }
180
181 // RegisterProtocol registers a protocol for automatic observability
182 func (s *CollectorState) RegisterProtocol(name string) *ProtocolMetrics {
183 pm := &ProtocolMetrics{Name: name}
184 s.protocols[name] = pm
185 return pm
186 }
187
188 // GetObsoleteInstances returns instances that became obsolete in this iteration
189 func (s *CollectorState) GetObsoleteInstances() []string {
190 return s.obsoleteInstances
191 }
192
193 // GetIteration returns the current global iteration counter
194 func (s *CollectorState) GetIteration() int64 {
195 return s.iteration
196 }
197
198 // Helper functions
199
200 func extractContextName(ctx any) string {
201 // Use reflection to get the Name field from Context[T]
202 v := reflect.ValueOf(ctx)
203 if v.Kind() == reflect.Pointer {
204 v = v.Elem()
205 }
206
207 if v.Kind() != reflect.Struct {
208 return ""
209 }
210
211 nameField := v.FieldByName("Name")
212 if nameField.IsValid() && nameField.Kind() == reflect.String {
213 return nameField.String()
214 }
215
216 return ""
217 }
218
219 func structToMap(labels any) map[string]string {
220 result := make(map[string]string)
221
222 if labels == nil {
223 return result
224 }
225
226 // Use reflection to extract struct fields
227 v := reflect.ValueOf(labels)
228 t := reflect.TypeOf(labels)
229
230 // Handle pointer types
231 if v.Kind() == reflect.Pointer {
232 if v.IsNil() {
233 return result
234 }
235 v = v.Elem()
236 t = t.Elem()
237 }
238
239 // Must be a struct
240 if v.Kind() != reflect.Struct {
241 return result
242 }
243
244 // Extract all exported fields from the struct
245 for i := 0; i < v.NumField(); i++ {
246 field := v.Field(i)
247 fieldType := t.Field(i)
248
249 // Skip unexported fields
250 if !fieldType.IsExported() {
251 continue
252 }
253
254 // Convert field name to lowercase for consistency
255 key := strings.ToLower(fieldType.Name)
256
257 // Convert field value to string
258 var value string
259 switch field.Kind() {
260 case reflect.String:
261 value = field.String()
262 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
263 value = fmt.Sprintf("%d", field.Int())
264 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
265 value = fmt.Sprintf("%d", field.Uint())
266 case reflect.Float32, reflect.Float64:
267 value = fmt.Sprintf("%.3f", field.Float())
268 case reflect.Bool:
269 value = fmt.Sprintf("%t", field.Bool())
270 default:
271 // For other types, use fmt.Sprintf as fallback
272 value = fmt.Sprintf("%v", field.Interface())
273 }
274
275 result[key] = value
276 }
277
278 return result
279 }
280
281 // generateInstanceKeyWithOrder creates instance key using hardcoded label order
282 // Format: {context}.{label_value1}_{label_value2}_...
283 // Label order is ALWAYS from the context definition, ensuring consistency
284 func generateInstanceKeyWithOrder(contextName string, labelKeys []string, labels map[string]string) string {
285 if len(labelKeys) == 0 || len(labels) == 0 {
286 return contextName
287 }
288
289 // Build label values in the EXACT order specified in context
290 labelValues := make([]string, 0, len(labelKeys))
291 for _, key := range labelKeys {
292 if value, ok := labels[strings.ToLower(key)]; ok {
293 labelValues = append(labelValues, cleanLabelValue(value))
294 }
295 }
296
297 if len(labelValues) > 0 {
298 return contextName + "." + strings.Join(labelValues, "_")
299 }
300
301 return contextName
302 }
303
304 func getContextMetadata(ctx any) *ContextMetadata {
305 // Extract metadata from Context[T] using reflection
306 return extractContextMetadata(ctx)
307 }
308
309 // extractContextMetadata extracts metadata from a Context[T] pointer
310 func extractContextMetadata(ctx any) *ContextMetadata {
311 v := reflect.ValueOf(ctx)
312 if v.Kind() == reflect.Pointer {
313 v = v.Elem()
314 }
315
316 if v.Kind() != reflect.Struct {
317 return nil
318 }
319
320 // Create metadata object
321 meta := &ContextMetadata{}
322
323 // Extract fields
324 if name := v.FieldByName("Name"); name.IsValid() && name.Kind() == reflect.String {
325 meta.Name = name.String()
326 }
327 if family := v.FieldByName("Family"); family.IsValid() && family.Kind() == reflect.String {
328 meta.Family = family.String()
329 }
330 if title := v.FieldByName("Title"); title.IsValid() && title.Kind() == reflect.String {
331 meta.Title = title.String()
332 }
333 if units := v.FieldByName("Units"); units.IsValid() && units.Kind() == reflect.String {
334 meta.Units = units.String()
335 }
336 if typ := v.FieldByName("Type"); typ.IsValid() {
337 // Type is module.ChartType (string)
338 if typ.Kind() == reflect.String {
339 meta.Type = collectorapi.ChartType(typ.String())
340 }
341 }
342 if priority := v.FieldByName("Priority"); priority.IsValid() && priority.Kind() == reflect.Int {
343 meta.Priority = int(priority.Int())
344 }
345 if updateEvery := v.FieldByName("UpdateEvery"); updateEvery.IsValid() && updateEvery.Kind() == reflect.Int {
346 meta.UpdateEvery = int(updateEvery.Int())
347 }
348
349 // Extract LabelKeys slice
350 if labelKeys := v.FieldByName("LabelKeys"); labelKeys.IsValid() && labelKeys.Kind() == reflect.Slice {
351 meta.HasLabels = labelKeys.Len() > 0
352 for i := 0; i < labelKeys.Len(); i++ {
353 if key := labelKeys.Index(i); key.Kind() == reflect.String {
354 meta.LabelOrder = append(meta.LabelOrder, key.String())
355 }
356 }
357 }
358
359 // Extract dimensions slice
360 if dims := v.FieldByName("Dimensions"); dims.IsValid() && dims.Kind() == reflect.Slice {
361 for i := 0; i < dims.Len(); i++ {
362 dim := dims.Index(i)
363 if dim.Kind() == reflect.Struct {
364 d := Dimension{}
365 if name := dim.FieldByName("Name"); name.IsValid() && name.Kind() == reflect.String {
366 d.Name = name.String()
367 }
368 if algo := dim.FieldByName("Algorithm"); algo.IsValid() {
369 // Algorithm is module.DimAlgo type (string)
370 if algo.Kind() == reflect.String {
371 d.Algorithm = collectorapi.DimAlgo(algo.String())
372 }
373 }
374 if mul := dim.FieldByName("Mul"); mul.IsValid() && mul.Kind() == reflect.Int {
375 d.Mul = int(mul.Int())
376 }
377 if div := dim.FieldByName("Div"); div.IsValid() && div.Kind() == reflect.Int {
378 d.Div = int(div.Int())
379 }
380 if precision := dim.FieldByName("Precision"); precision.IsValid() && precision.Kind() == reflect.Int {
381 d.Precision = int(precision.Int())
382 }
383 meta.Dimensions = append(meta.Dimensions, d)
384 }
385 }
386 }
387
388 return meta
389 }
390
391 func getContextUpdateInterval(contextName string) int {
392 // Get update interval for context
393 // This will look up from registered contexts
394 return 1
395 }
396
397 // Debugf logs a debug message (delegate to collector's logger)
398 func (s *CollectorState) Debugf(format string, args ...any) {
399 if s.collector != nil {
400 s.collector.Debugf(format, args...)
401 }
402 }
403
404 // Warningf logs a warning message (delegate to collector's logger)
405 func (s *CollectorState) Warningf(format string, args ...any) {
406 if s.collector != nil {
407 s.collector.Warningf(format, args...)
408 }
409 }
410
411 // Errorf logs an error message (delegate to collector's logger)
412 func (s *CollectorState) Errorf(format string, args ...any) {
413 if s.collector != nil {
414 s.collector.Errorf(format, args...)
415 }
416 }
417
418 // Infof logs an info message (delegate to collector's logger)
419 func (s *CollectorState) Infof(format string, args ...any) {
420 if s.collector != nil {
421 s.collector.Infof(format, args...)
422 }
423 }