master
go 707 lines 19.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartengine
4
5 import (
6 "fmt"
7 "strconv"
8 "strings"
9
10 "github.com/netdata/netdata/go/plugins/pkg/metrix"
11 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine/internal/program"
12 )
13
14 const (
15 autogenTemplatePrefix = "__autogen__:"
16 )
17
18 type autogenRoute struct {
19 chartID string
20 chartName string
21 title string
22 dimensionName string
23 dimensionKeyLabel string
24 algorithm program.Algorithm
25 units string
26 chartType program.ChartType
27 family string
28 priority int
29 contextName string
30 staticDimension bool
31 float bool
32 }
33
34 type autogenSourceBuilder func(
35 metricName string,
36 labels metrix.LabelView,
37 meta metrix.SeriesMeta,
38 policy AutogenPolicy,
39 typeIDPrefix string,
40 ) (autogenRoute, bool, error)
41
42 type autogenRoleBuilder func(
43 metricName string,
44 labels metrix.LabelView,
45 policy AutogenPolicy,
46 typeIDPrefix string,
47 ) (autogenRoute, bool, error)
48
49 var autogenSourceBuilders = map[metrix.MetricKind]autogenSourceBuilder{
50 metrix.MetricKindHistogram: buildHistogramAutogenRoute,
51 metrix.MetricKindSummary: buildSummaryAutogenRoute,
52 metrix.MetricKindStateSet: buildStateSetAutogenRoute,
53 metrix.MetricKindMeasureSet: buildMeasureSetAutogenRoute,
54 }
55
56 var histogramRoleBuilders = map[metrix.FlattenRole]autogenRoleBuilder{
57 metrix.FlattenRoleHistogramBucket: buildHistogramBucketAutogenRoute,
58 metrix.FlattenRoleHistogramCount: buildHistogramCountAutogenRoute,
59 metrix.FlattenRoleHistogramSum: buildHistogramSumAutogenRoute,
60 }
61
62 var summaryRoleBuilders = map[metrix.FlattenRole]autogenRoleBuilder{
63 metrix.FlattenRoleSummaryQuantile: buildSummaryQuantileAutogenRoute,
64 metrix.FlattenRoleSummaryCount: buildSummaryCountAutogenRoute,
65 metrix.FlattenRoleSummarySum: buildSummarySumAutogenRoute,
66 }
67
68 func (e *Engine) resolveAutogenRoute(
69 reader metrix.Reader,
70 metricName string,
71 labels metrix.LabelView,
72 meta metrix.SeriesMeta,
73 ) ([]routeBinding, bool, error) {
74 if e == nil {
75 return nil, false, fmt.Errorf("chartengine: nil engine")
76 }
77 policy := e.state.cfg.autogen
78 if !policy.Enabled {
79 return nil, false, nil
80 }
81
82 namespace := e.state.cfg.autogenContextNamespace
83
84 route, ok, err := buildAutogenRoute(metricName, labels, meta, policy, e.state.cfg.autogenTypeID)
85 if err != nil {
86 return nil, false, err
87 }
88 if !ok {
89 return nil, false, nil
90 }
91 if metricMeta, ok := autogenMetricMeta(reader, metricName, meta); ok {
92 route = applyAutogenMetricMeta(route, metricMeta, meta)
93 }
94 route.priority = effectiveChartPriority(route.priority)
95 title := route.title
96 if title == "" {
97 title = getAutogenChartTitle(route.chartName)
98 }
99 return []routeBinding{
100 {
101 ChartTemplateID: autogenTemplatePrefix + route.chartID,
102 ChartID: route.chartID,
103 DimensionIndex: 0,
104 DimensionName: route.dimensionName,
105 DimensionKeyLabel: route.dimensionKeyLabel,
106 Algorithm: route.algorithm,
107 Hidden: false,
108 Multiplier: 1,
109 Divisor: 1,
110 Float: route.float,
111 Static: route.staticDimension,
112 Inferred: false,
113 Autogen: true,
114 Meta: program.ChartMeta{
115 Title: title,
116 Family: route.family,
117 Context: getAutogenChartContext(namespace, route.contextName),
118 Units: route.units,
119 Algorithm: route.algorithm,
120 Type: route.chartType,
121 Priority: route.priority,
122 },
123 Lifecycle: autogenLifecyclePolicy(policy),
124 },
125 }, true, nil
126 }
127
128 func buildAutogenRoute(
129 metricName string,
130 labels metrix.LabelView,
131 meta metrix.SeriesMeta,
132 policy AutogenPolicy,
133 typeIDPrefix string,
134 ) (autogenRoute, bool, error) {
135 if strings.TrimSpace(metricName) == "" {
136 return autogenRoute{}, false, nil
137 }
138 builder := buildScalarAutogenRoute
139 if knownBuilder, ok := autogenSourceBuilders[meta.SourceKind]; ok {
140 builder = knownBuilder
141 }
142 return builder(metricName, labels, meta, policy, typeIDPrefix)
143 }
144
145 func autogenMetricMeta(reader metrix.Reader, metricName string, meta metrix.SeriesMeta) (metrix.MetricMeta, bool) {
146 if reader == nil {
147 return metrix.MetricMeta{}, false
148 }
149 for _, name := range sourceMetricNames(metricName, meta) {
150 if name == "" {
151 continue
152 }
153 if metricMeta, ok := reader.MetricMeta(name); ok {
154 return metricMeta, true
155 }
156 }
157 return metrix.MetricMeta{}, false
158 }
159
160 func sourceMetricNames(metricName string, meta metrix.SeriesMeta) []string {
161 source := sourceMetricName(metricName, meta)
162 if source == "" || source == metricName {
163 return []string{metricName}
164 }
165 return []string{source, metricName}
166 }
167
168 func sourceMetricName(metricName string, meta metrix.SeriesMeta) string {
169 switch meta.SourceKind {
170 case metrix.MetricKindHistogram:
171 switch meta.FlattenRole {
172 case metrix.FlattenRoleHistogramBucket:
173 return strings.TrimSuffix(metricName, "_bucket")
174 case metrix.FlattenRoleHistogramCount:
175 return strings.TrimSuffix(metricName, "_count")
176 case metrix.FlattenRoleHistogramSum:
177 return strings.TrimSuffix(metricName, "_sum")
178 }
179 case metrix.MetricKindSummary:
180 switch meta.FlattenRole {
181 case metrix.FlattenRoleSummaryCount:
182 return strings.TrimSuffix(metricName, "_count")
183 case metrix.FlattenRoleSummarySum:
184 return strings.TrimSuffix(metricName, "_sum")
185 }
186 }
187 return metricName
188 }
189
190 func applyAutogenMetricMeta(route autogenRoute, meta metrix.MetricMeta, seriesMeta metrix.SeriesMeta) autogenRoute {
191 if title := strings.TrimSpace(meta.Description); title != "" {
192 route.title = title
193 }
194 if family := strings.TrimSpace(meta.ChartFamily); family != "" {
195 route.family = family
196 }
197 if meta.ChartPriority > 0 {
198 route.priority = meta.ChartPriority
199 }
200 if unit := strings.TrimSpace(meta.Unit); unit != "" && allowAutogenUnitOverride(seriesMeta) {
201 route.units = normalizeAutogenUnitByAlgorithm(unit, route.algorithm)
202 route.chartType = chartTypeFromUnits(route.units)
203 }
204 route.float = meta.Float
205 return route
206 }
207
208 func allowAutogenUnitOverride(meta metrix.SeriesMeta) bool {
209 if meta.SourceKind == metrix.MetricKindStateSet {
210 return false
211 }
212 if meta.SourceKind == metrix.MetricKindHistogram {
213 return meta.FlattenRole != metrix.FlattenRoleHistogramBucket &&
214 meta.FlattenRole != metrix.FlattenRoleHistogramCount
215 }
216 if meta.SourceKind == metrix.MetricKindSummary {
217 return meta.FlattenRole != metrix.FlattenRoleSummaryCount
218 }
219 return true
220 }
221
222 func normalizeAutogenUnitByAlgorithm(unit string, alg program.Algorithm) string {
223 unit = strings.TrimSpace(unit)
224 if unit == "" {
225 return unit
226 }
227 if alg != program.AlgorithmIncremental {
228 return unit
229 }
230 switch unit {
231 case "seconds", "time":
232 return unit
233 }
234 if strings.HasSuffix(unit, "/s") {
235 return unit
236 }
237 return unit + "/s"
238 }
239
240 func buildHistogramAutogenRoute(
241 metricName string,
242 labels metrix.LabelView,
243 meta metrix.SeriesMeta,
244 policy AutogenPolicy,
245 typeIDPrefix string,
246 ) (autogenRoute, bool, error) {
247 builder, ok := histogramRoleBuilders[meta.FlattenRole]
248 if !ok {
249 return autogenRoute{}, false, nil
250 }
251 return builder(metricName, labels, policy, typeIDPrefix)
252 }
253
254 func buildSummaryAutogenRoute(
255 metricName string,
256 labels metrix.LabelView,
257 meta metrix.SeriesMeta,
258 policy AutogenPolicy,
259 typeIDPrefix string,
260 ) (autogenRoute, bool, error) {
261 builder, ok := summaryRoleBuilders[meta.FlattenRole]
262 if !ok {
263 return autogenRoute{}, false, nil
264 }
265 return builder(metricName, labels, policy, typeIDPrefix)
266 }
267
268 func buildHistogramBucketAutogenRoute(
269 metricName string,
270 labels metrix.LabelView,
271 policy AutogenPolicy,
272 typeIDPrefix string,
273 ) (autogenRoute, bool, error) {
274 baseName := strings.TrimSuffix(metricName, "_bucket")
275 if baseName == "" {
276 baseName = metricName
277 }
278 upperBound, ok := labels.Get(metrix.HistogramBucketLabel)
279 if !ok || strings.TrimSpace(upperBound) == "" {
280 return autogenRoute{}, false, nil
281 }
282 chartID := buildJoinedLabelAutogenID(baseName, labels, map[string]struct{}{
283 metrix.HistogramBucketLabel: {},
284 })
285 if !fitsTypeIDBudget(policy.MaxTypeIDLen, typeIDPrefix, chartID) {
286 return autogenRoute{}, false, nil
287 }
288 return autogenRoute{
289 chartID: chartID,
290 chartName: baseName,
291 dimensionName: "bucket_" + upperBound,
292 dimensionKeyLabel: metrix.HistogramBucketLabel,
293 algorithm: program.AlgorithmIncremental,
294 units: "observations/s",
295 chartType: program.ChartTypeLine,
296 family: getAutogenChartFamily(baseName),
297 contextName: baseName,
298 staticDimension: false,
299 }, true, nil
300 }
301
302 func buildHistogramCountAutogenRoute(
303 metricName string,
304 labels metrix.LabelView,
305 policy AutogenPolicy,
306 typeIDPrefix string,
307 ) (autogenRoute, bool, error) {
308 baseName := strings.TrimSuffix(metricName, "_count")
309 if baseName == "" {
310 baseName = metricName
311 }
312 return buildCounterComponentAutogenRoute(baseName, "_count", labels, policy, typeIDPrefix, "events/s")
313 }
314
315 func buildHistogramSumAutogenRoute(
316 metricName string,
317 labels metrix.LabelView,
318 policy AutogenPolicy,
319 typeIDPrefix string,
320 ) (autogenRoute, bool, error) {
321 baseName := strings.TrimSuffix(metricName, "_sum")
322 if baseName == "" {
323 baseName = metricName
324 }
325 return buildCounterComponentAutogenRoute(baseName, "_sum", labels, policy, typeIDPrefix, getAutogenCounterUnits(baseName))
326 }
327
328 func buildSummaryQuantileAutogenRoute(
329 metricName string,
330 labels metrix.LabelView,
331 policy AutogenPolicy,
332 typeIDPrefix string,
333 ) (autogenRoute, bool, error) {
334 quantile, ok := labels.Get(metrix.SummaryQuantileLabel)
335 if !ok || strings.TrimSpace(quantile) == "" {
336 return autogenRoute{}, false, nil
337 }
338 chartID := buildJoinedLabelAutogenID(metricName, labels, map[string]struct{}{
339 metrix.SummaryQuantileLabel: {},
340 })
341 if !fitsTypeIDBudget(policy.MaxTypeIDLen, typeIDPrefix, chartID) {
342 return autogenRoute{}, false, nil
343 }
344 units := getAutogenSummaryUnits(metricName)
345 return autogenRoute{
346 chartID: chartID,
347 chartName: metricName,
348 dimensionName: "quantile_" + quantile,
349 dimensionKeyLabel: metrix.SummaryQuantileLabel,
350 algorithm: program.AlgorithmAbsolute,
351 units: units,
352 chartType: chartTypeFromUnits(units),
353 family: getAutogenChartFamily(metricName),
354 contextName: metricName,
355 staticDimension: false,
356 }, true, nil
357 }
358
359 func buildSummaryCountAutogenRoute(
360 metricName string,
361 labels metrix.LabelView,
362 policy AutogenPolicy,
363 typeIDPrefix string,
364 ) (autogenRoute, bool, error) {
365 baseName := strings.TrimSuffix(metricName, "_count")
366 if baseName == "" {
367 baseName = metricName
368 }
369 return buildCounterComponentAutogenRoute(baseName, "_count", labels, policy, typeIDPrefix, "events/s")
370 }
371
372 func buildSummarySumAutogenRoute(
373 metricName string,
374 labels metrix.LabelView,
375 policy AutogenPolicy,
376 typeIDPrefix string,
377 ) (autogenRoute, bool, error) {
378 baseName := strings.TrimSuffix(metricName, "_sum")
379 if baseName == "" {
380 baseName = metricName
381 }
382 return buildCounterComponentAutogenRoute(baseName, "_sum", labels, policy, typeIDPrefix, getAutogenCounterUnits(baseName))
383 }
384
385 func buildCounterComponentAutogenRoute(
386 baseName string,
387 suffix string,
388 labels metrix.LabelView,
389 policy AutogenPolicy,
390 typeIDPrefix string,
391 units string,
392 ) (autogenRoute, bool, error) {
393 chartName := baseName + suffix
394 chartID := buildJoinedLabelAutogenID(chartName, labels, nil)
395 if !fitsTypeIDBudget(policy.MaxTypeIDLen, typeIDPrefix, chartID) {
396 return autogenRoute{}, false, nil
397 }
398 return autogenRoute{
399 chartID: chartID,
400 chartName: baseName,
401 dimensionName: autogenDimensionName(chartName),
402 algorithm: program.AlgorithmIncremental,
403 units: units,
404 chartType: chartTypeFromUnits(units),
405 family: getAutogenChartFamily(baseName),
406 contextName: chartName,
407 staticDimension: true,
408 }, true, nil
409 }
410
411 func buildStateSetAutogenRoute(
412 metricName string,
413 labels metrix.LabelView,
414 meta metrix.SeriesMeta,
415 policy AutogenPolicy,
416 typeIDPrefix string,
417 ) (autogenRoute, bool, error) {
418 if meta.FlattenRole != metrix.FlattenRoleStateSetState {
419 return autogenRoute{}, false, nil
420 }
421 state, ok := labels.Get(metricName)
422 if !ok || strings.TrimSpace(state) == "" {
423 return autogenRoute{}, false, nil
424 }
425 chartID := buildJoinedLabelAutogenID(metricName, labels, map[string]struct{}{
426 metricName: {},
427 })
428 if !fitsTypeIDBudget(policy.MaxTypeIDLen, typeIDPrefix, chartID) {
429 return autogenRoute{}, false, nil
430 }
431 return autogenRoute{
432 chartID: chartID,
433 chartName: metricName,
434 dimensionName: state,
435 dimensionKeyLabel: metricName,
436 algorithm: program.AlgorithmAbsolute,
437 units: "state",
438 chartType: program.ChartTypeLine,
439 family: getAutogenChartFamily(metricName),
440 contextName: metricName,
441 staticDimension: false,
442 }, true, nil
443 }
444
445 func buildMeasureSetAutogenRoute(
446 metricName string,
447 labels metrix.LabelView,
448 meta metrix.SeriesMeta,
449 policy AutogenPolicy,
450 typeIDPrefix string,
451 ) (autogenRoute, bool, error) {
452 if meta.FlattenRole != metrix.FlattenRoleMeasureSetField {
453 return autogenRoute{}, false, nil
454 }
455 sourceName, fieldName, ok := resolveMeasureSetAutogenSource(metricName, labels)
456 if !ok {
457 return autogenRoute{}, false, nil
458 }
459 chartID := buildJoinedLabelAutogenID(sourceName, labels, map[string]struct{}{
460 metrix.MeasureSetFieldLabel: {},
461 })
462 if !fitsTypeIDBudget(policy.MaxTypeIDLen, typeIDPrefix, chartID) {
463 return autogenRoute{}, false, nil
464 }
465 algorithm := program.AlgorithmAbsolute
466 units := getAutogenGaugeUnits(sourceName)
467 if meta.Kind == metrix.MetricKindCounter {
468 algorithm = program.AlgorithmIncremental
469 units = getAutogenCounterUnits(sourceName)
470 }
471 return autogenRoute{
472 chartID: chartID,
473 chartName: sourceName,
474 dimensionName: fieldName,
475 dimensionKeyLabel: metrix.MeasureSetFieldLabel,
476 algorithm: algorithm,
477 units: units,
478 chartType: chartTypeFromUnits(units),
479 family: getAutogenChartFamily(sourceName),
480 contextName: sourceName,
481 staticDimension: false,
482 }, true, nil
483 }
484
485 func resolveMeasureSetAutogenSource(metricName string, labels metrix.LabelView) (string, string, bool) {
486 if strings.TrimSpace(metricName) == "" {
487 return "", "", false
488 }
489 if labels == nil {
490 return "", "", false
491 }
492
493 fieldName, ok := labels.Get(metrix.MeasureSetFieldLabel)
494 if !ok || strings.TrimSpace(fieldName) == "" {
495 return "", "", false
496 }
497
498 suffix := "_" + fieldName
499 if !strings.HasSuffix(metricName, suffix) {
500 return "", "", false
501 }
502
503 sourceName := strings.TrimSuffix(metricName, suffix)
504 if strings.TrimSpace(sourceName) == "" {
505 return "", "", false
506 }
507 return sourceName, fieldName, true
508 }
509
510 func buildScalarAutogenRoute(
511 metricName string,
512 labels metrix.LabelView,
513 meta metrix.SeriesMeta,
514 policy AutogenPolicy,
515 typeIDPrefix string,
516 ) (autogenRoute, bool, error) {
517 chartID := buildJoinedLabelAutogenID(metricName, labels, nil)
518 if !fitsTypeIDBudget(policy.MaxTypeIDLen, typeIDPrefix, chartID) {
519 return autogenRoute{}, false, nil
520 }
521 algorithm := program.AlgorithmAbsolute
522 units := getAutogenGaugeUnits(metricName)
523 if meta.Kind == metrix.MetricKindCounter {
524 algorithm = program.AlgorithmIncremental
525 units = getAutogenCounterUnits(metricName)
526 }
527 return autogenRoute{
528 chartID: chartID,
529 chartName: metricName,
530 dimensionName: autogenDimensionName(metricName),
531 algorithm: algorithm,
532 units: units,
533 chartType: chartTypeFromUnits(units),
534 family: getAutogenChartFamily(metricName),
535 contextName: metricName,
536 staticDimension: true,
537 }, true, nil
538 }
539
540 func fitsTypeIDBudget(maxLen int, typeIDPrefix, chartID string) bool {
541 if maxLen <= 0 {
542 maxLen = defaultMaxTypeIDLen
543 }
544 if strings.TrimSpace(typeIDPrefix) == "" {
545 return len(chartID) <= maxLen
546 }
547 return len(typeIDPrefix)+1+len(chartID) <= maxLen
548 }
549
550 func buildJoinedLabelAutogenID(metricName string, labels metrix.LabelView, exclude map[string]struct{}) string {
551 var b strings.Builder
552 b.Grow(len(metricName) + labels.Len()*8)
553 b.WriteString(metricName)
554 labels.Range(func(key, value string) bool {
555 if key == "" || value == "" {
556 return true
557 }
558 if _, skip := exclude[key]; skip {
559 return true
560 }
561 b.WriteByte('-')
562 b.WriteString(key)
563 b.WriteByte('=')
564 b.WriteString(normalizeAutogenLabelValue(value))
565 return true
566 })
567 return b.String()
568 }
569
570 func normalizeAutogenLabelValue(value string) string {
571 if strings.IndexByte(value, '\\') != -1 {
572 v := decodeAutogenLabelValue(value)
573 switch {
574 case strings.IndexByte(v, '\\') != -1:
575 value = strings.ReplaceAll(v, `\`, "_")
576 case hasControlChars(v):
577 // Keep raw when unquoting introduces controls (e.g. \b => backspace).
578 default:
579 value = v
580 }
581 }
582 return sanitizeChartIDLabelValue(value)
583 }
584
585 func hasControlChars(value string) bool {
586 for _, r := range value {
587 if r < 0x20 || r == 0x7f {
588 return true
589 }
590 }
591 return false
592 }
593
594 func decodeAutogenLabelValue(value string) string {
595 v, err := strconv.Unquote("\"" + value + "\"")
596 if err != nil {
597 return value
598 }
599 return v
600 }
601
602 func getAutogenChartTitle(metricName string) string {
603 return fmt.Sprintf("Metric \"%s\"", metricName)
604 }
605
606 // getAutogenChartContext builds an autogen chart context, prefixed by the spec's root
607 // context_namespace when set ("prometheus" + "foo" -> "prometheus.foo"), matching the
608 // template compiler's "." join. Empty namespace -> the bare metric name (unchanged).
609 func getAutogenChartContext(namespace, metricName string) string {
610 metricName = strings.TrimSpace(metricName)
611 if metricName == "" {
612 metricName = "metric"
613 }
614 if namespace = strings.TrimSpace(namespace); namespace != "" {
615 return namespace + "." + metricName
616 }
617 return metricName
618 }
619
620 func autogenDimensionName(metricName string) string {
621 metricName = strings.TrimSpace(metricName)
622 if metricName == "" {
623 return metricName
624 }
625 idx := strings.LastIndexByte(metricName, '.')
626 if idx == -1 || idx == len(metricName)-1 {
627 return metricName
628 }
629 return metricName[idx+1:]
630 }
631
632 func getAutogenChartFamily(metric string) string {
633 if strings.HasPrefix(metric, "go_") {
634 return "go"
635 }
636 if strings.HasPrefix(metric, "process_") {
637 return "process"
638 }
639 parts := strings.SplitN(metric, "_", 3)
640 family := metric
641 if len(parts) >= 3 {
642 family = parts[0] + "_" + parts[1]
643 }
644 i := len(family) - 1
645 for i >= 0 && family[i] >= '0' && family[i] <= '9' {
646 i--
647 }
648 if i > 0 {
649 return family[:i+1]
650 }
651 return family
652 }
653
654 func getAutogenGaugeUnits(metric string) string {
655 return getAutogenMetricUnits(metric)
656 }
657
658 func getAutogenCounterUnits(metric string) string {
659 units := getAutogenMetricUnits(metric)
660 switch units {
661 case "seconds", "time":
662 default:
663 units += "/s"
664 }
665 return units
666 }
667
668 func getAutogenSummaryUnits(metric string) string {
669 return getAutogenGaugeUnits(metric)
670 }
671
672 func getAutogenMetricUnits(metric string) string {
673 idx := strings.LastIndexByte(metric, '_')
674 if idx == -1 {
675 return "events"
676 }
677 switch suffix := metric[idx:]; suffix {
678 case "_total", "_sum", "_count", "_ratio":
679 return getAutogenMetricUnits(metric[:idx])
680 }
681 switch units := metric[idx+1:]; units {
682 case "hertz":
683 return "Hz"
684 default:
685 return units
686 }
687 }
688
689 func chartTypeFromUnits(units string) program.ChartType {
690 if strings.HasSuffix(units, "bytes") || strings.HasSuffix(units, "bytes/s") {
691 return program.ChartTypeArea
692 }
693 return program.ChartTypeLine
694 }
695
696 func autogenLifecyclePolicy(policy AutogenPolicy) program.LifecyclePolicy {
697 expire := 0
698 if policy.ExpireAfterSuccessCycles > 0 {
699 expire = int(policy.ExpireAfterSuccessCycles)
700 }
701 return program.LifecyclePolicy{
702 ExpireAfterCycles: expire,
703 Dimensions: program.DimensionLifecyclePolicy{
704 ExpireAfterCycles: expire,
705 },
706 }
707 }