| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package program |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | ) |
| 9 | |
| 10 | // Algorithm defines how Netdata interprets values on wire. |
| 11 | type Algorithm string |
| 12 | |
| 13 | const ( |
| 14 | // AlgorithmAbsolute sends direct values. |
| 15 | AlgorithmAbsolute Algorithm = "absolute" |
| 16 | // AlgorithmIncremental sends monotonic totals (Netdata computes rates/deltas). |
| 17 | AlgorithmIncremental Algorithm = "incremental" |
| 18 | ) |
| 19 | |
| 20 | // ChartType controls chart visualization. |
| 21 | type ChartType string |
| 22 | |
| 23 | const ( |
| 24 | ChartTypeLine ChartType = "line" |
| 25 | ChartTypeArea ChartType = "area" |
| 26 | ChartTypeStacked ChartType = "stacked" |
| 27 | ChartTypeHeatmap ChartType = "heatmap" |
| 28 | ) |
| 29 | |
| 30 | // ReduceOp defines aggregation for series collisions mapped to one dimension. |
| 31 | type ReduceOp string |
| 32 | |
| 33 | const ( |
| 34 | // ReduceSum is the phase-1 collision reduction rule. |
| 35 | ReduceSum ReduceOp = "sum" |
| 36 | ) |
| 37 | |
| 38 | // Chart is one compiled chart template in immutable program IR. |
| 39 | type Chart struct { |
| 40 | // TemplateID is compiler-assigned stable ID inside one Program revision. |
| 41 | TemplateID string |
| 42 | |
| 43 | Meta ChartMeta |
| 44 | Identity ChartIdentity |
| 45 | Labels LabelPolicy |
| 46 | Lifecycle LifecyclePolicy |
| 47 | |
| 48 | // Dimensions are declaration-ordered templates. |
| 49 | Dimensions []Dimension |
| 50 | |
| 51 | // CollisionReduce is currently fixed to ReduceSum in phase-1. |
| 52 | CollisionReduce ReduceOp |
| 53 | } |
| 54 | |
| 55 | // ChartMeta carries user-facing chart metadata after normalization/defaulting. |
| 56 | type ChartMeta struct { |
| 57 | Title string |
| 58 | Family string |
| 59 | Context string |
| 60 | Units string |
| 61 | Algorithm Algorithm |
| 62 | Type ChartType |
| 63 | Priority int |
| 64 | } |
| 65 | |
| 66 | // ChartIdentity describes how chart instances are derived. |
| 67 | // |
| 68 | // Phase-1 uses literal chart IDs and optional instance suffix derivation from |
| 69 | // configured labels. |
| 70 | type ChartIdentity struct { |
| 71 | // IDTemplate is a normalized literal chart ID. |
| 72 | IDTemplate Template |
| 73 | |
| 74 | // InstanceByLabels contains resolved explicit identity selectors (if used). |
| 75 | InstanceByLabels []InstanceLabelSelector |
| 76 | |
| 77 | // ContextNamespace holds normalized namespace fragments that participate in |
| 78 | // derived context/id building in namespace-based authoring mode. |
| 79 | ContextNamespace []string |
| 80 | |
| 81 | // Static is true when identity renders one aggregated chart instance. |
| 82 | Static bool |
| 83 | } |
| 84 | |
| 85 | func validateChart(chart Chart) error { |
| 86 | var errs []error |
| 87 | if chart.TemplateID == "" { |
| 88 | errs = append(errs, fmt.Errorf("template_id is required")) |
| 89 | } |
| 90 | if chart.Meta.Context == "" { |
| 91 | errs = append(errs, fmt.Errorf("context is required")) |
| 92 | } |
| 93 | if chart.Meta.Units == "" { |
| 94 | errs = append(errs, fmt.Errorf("units is required")) |
| 95 | } |
| 96 | if chart.Meta.Algorithm != AlgorithmAbsolute && chart.Meta.Algorithm != AlgorithmIncremental { |
| 97 | errs = append(errs, fmt.Errorf("invalid algorithm %q", chart.Meta.Algorithm)) |
| 98 | } |
| 99 | switch chart.Meta.Type { |
| 100 | case ChartTypeLine, ChartTypeArea, ChartTypeStacked, ChartTypeHeatmap: |
| 101 | default: |
| 102 | errs = append(errs, fmt.Errorf("invalid chart type %q", chart.Meta.Type)) |
| 103 | } |
| 104 | if err := validateInstanceLabelSelectors(chart.Identity.InstanceByLabels); err != nil { |
| 105 | errs = append(errs, fmt.Errorf("identity: %w", err)) |
| 106 | } |
| 107 | if err := validateLabelPolicy(chart.Labels); err != nil { |
| 108 | errs = append(errs, fmt.Errorf("labels: %w", err)) |
| 109 | } |
| 110 | if chart.CollisionReduce == "" { |
| 111 | errs = append(errs, fmt.Errorf("collision reduce op is required")) |
| 112 | } |
| 113 | if len(chart.Dimensions) == 0 { |
| 114 | errs = append(errs, fmt.Errorf("at least one dimension is required")) |
| 115 | } |
| 116 | for i, dim := range chart.Dimensions { |
| 117 | if err := validateDimension(dim); err != nil { |
| 118 | errs = append(errs, fmt.Errorf("dimension[%d]: %w", i, err)) |
| 119 | } |
| 120 | } |
| 121 | return errors.Join(errs...) |
| 122 | } |
| 123 | |
| 124 | func (c Chart) clone() Chart { |
| 125 | out := c |
| 126 | out.Meta = c.Meta |
| 127 | out.Identity = c.Identity.clone() |
| 128 | out.Labels = c.Labels.clone() |
| 129 | out.Lifecycle = c.Lifecycle.clone() |
| 130 | |
| 131 | out.Dimensions = make([]Dimension, 0, len(c.Dimensions)) |
| 132 | for _, dim := range c.Dimensions { |
| 133 | out.Dimensions = append(out.Dimensions, dim.clone()) |
| 134 | } |
| 135 | return out |
| 136 | } |
| 137 | |
| 138 | func (i ChartIdentity) clone() ChartIdentity { |
| 139 | out := i |
| 140 | out.IDTemplate = i.IDTemplate.clone() |
| 141 | |
| 142 | out.InstanceByLabels = make([]InstanceLabelSelector, 0, len(i.InstanceByLabels)) |
| 143 | for _, selector := range i.InstanceByLabels { |
| 144 | out.InstanceByLabels = append(out.InstanceByLabels, selector.clone()) |
| 145 | } |
| 146 | out.ContextNamespace = append([]string(nil), i.ContextNamespace...) |
| 147 | return out |
| 148 | } |