| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package chartengine |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | |
| 8 | "github.com/netdata/netdata/go/plugins/logger" |
| 9 | "github.com/netdata/netdata/go/plugins/pkg/metrix" |
| 10 | metrixselector "github.com/netdata/netdata/go/plugins/pkg/metrix/selector" |
| 11 | "github.com/netdata/netdata/go/plugins/plugin/framework/runtimecomp" |
| 12 | ) |
| 13 | |
| 14 | type engineConfig struct { |
| 15 | autogen AutogenPolicy |
| 16 | autogenTypeID string |
| 17 | // autogenContextNamespace prefixes autogen chart contexts (the spec's root |
| 18 | // context_namespace), so autogen and template charts share one namespace. |
| 19 | autogenContextNamespace string |
| 20 | selector metrixselector.Selector |
| 21 | autogenOverride policyOverride[AutogenPolicy] |
| 22 | selectorOverride policyOverride[metrixselector.Selector] |
| 23 | runtimeStore metrix.RuntimeStore |
| 24 | runtimeStoreSet bool |
| 25 | runtimeObserver func(PlanRuntimeSample) |
| 26 | log *logger.Logger |
| 27 | seriesSelection seriesSelectionMode |
| 28 | runtimePlanner bool |
| 29 | } |
| 30 | |
| 31 | type policyOverride[T any] struct { |
| 32 | set bool |
| 33 | value T |
| 34 | } |
| 35 | |
| 36 | // Option mutates engine configuration at construction time. |
| 37 | type Option func(*engineConfig) error |
| 38 | |
| 39 | const ( |
| 40 | defaultMaxTypeIDLen = 1200 |
| 41 | ) |
| 42 | |
| 43 | type seriesSelectionMode uint8 |
| 44 | |
| 45 | const ( |
| 46 | seriesSelectionLastSuccessOnly seriesSelectionMode = iota |
| 47 | seriesSelectionAllVisible |
| 48 | ) |
| 49 | |
| 50 | // AutogenPolicy controls unmatched-series fallback chart generation. |
| 51 | // It aliases runtime component policy to keep one policy contract. |
| 52 | type AutogenPolicy = runtimecomp.AutogenPolicy |
| 53 | |
| 54 | // EnginePolicy controls chartengine matching/materialization behavior. |
| 55 | type EnginePolicy struct { |
| 56 | // Selector filters input series globally before template/autogen routing. |
| 57 | // Nil means "no override", and an explicitly empty expr means "override to allow all". |
| 58 | Selector *metrixselector.Expr |
| 59 | |
| 60 | // Autogen controls unmatched-series fallback behavior. |
| 61 | Autogen *AutogenPolicy |
| 62 | } |
| 63 | |
| 64 | func defaultAutogenPolicy() AutogenPolicy { |
| 65 | return AutogenPolicy{ |
| 66 | Enabled: false, |
| 67 | MaxTypeIDLen: defaultMaxTypeIDLen, |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func normalizeAutogenPolicy(policy AutogenPolicy) (AutogenPolicy, error) { |
| 72 | maxLen := policy.MaxTypeIDLen |
| 73 | if maxLen <= 0 { |
| 74 | maxLen = defaultMaxTypeIDLen |
| 75 | } |
| 76 | if maxLen < 4 { |
| 77 | return AutogenPolicy{}, fmt.Errorf("autogen max type.id len must be >= 4, got %d", maxLen) |
| 78 | } |
| 79 | policy.MaxTypeIDLen = maxLen |
| 80 | return policy, nil |
| 81 | } |
| 82 | |
| 83 | func compileEngineSelector(expr metrixselector.Expr) (metrixselector.Selector, error) { |
| 84 | if expr.Empty() { |
| 85 | return nil, nil |
| 86 | } |
| 87 | return expr.Parse() |
| 88 | } |
| 89 | |
| 90 | // WithEnginePolicy configures chartengine matching/materialization policy. |
| 91 | func WithEnginePolicy(policy EnginePolicy) Option { |
| 92 | return func(cfg *engineConfig) error { |
| 93 | if policy.Autogen != nil { |
| 94 | autogen, err := normalizeAutogenPolicy(*policy.Autogen) |
| 95 | if err != nil { |
| 96 | return err |
| 97 | } |
| 98 | cfg.autogenOverride = policyOverride[AutogenPolicy]{set: true, value: autogen} |
| 99 | cfg.autogen = autogen |
| 100 | } |
| 101 | if policy.Selector != nil { |
| 102 | selector, err := compileEngineSelector(*policy.Selector) |
| 103 | if err != nil { |
| 104 | return fmt.Errorf("invalid engine selector: %w", err) |
| 105 | } |
| 106 | cfg.selectorOverride = policyOverride[metrixselector.Selector]{set: true, value: selector} |
| 107 | cfg.selector = selector |
| 108 | } |
| 109 | return nil |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // WithRuntimeStore configures internal chartengine runtime metrics store. |
| 114 | // Passing nil disables chartengine self-metrics. |
| 115 | func WithRuntimeStore(store metrix.RuntimeStore) Option { |
| 116 | return func(cfg *engineConfig) error { |
| 117 | cfg.runtimeStore = store |
| 118 | cfg.runtimeStoreSet = true |
| 119 | return nil |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // WithRuntimeSampleObserver configures a callback for per-build runtime samples. |
| 124 | // |
| 125 | // The callback fires for successful builds, build errors, and collect-status |
| 126 | // skips. It does not fire for pre-build contract errors such as an outstanding |
| 127 | // plan attempt. |
| 128 | // |
| 129 | // The callback is in addition to WithRuntimeStore. Pass WithRuntimeStore(nil) |
| 130 | // when samples are aggregated elsewhere and the engine should not write its own |
| 131 | // runtime metrics. |
| 132 | func WithRuntimeSampleObserver(fn func(PlanRuntimeSample)) Option { |
| 133 | return func(cfg *engineConfig) error { |
| 134 | cfg.runtimeObserver = fn |
| 135 | return nil |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | // WithLogger configures chartengine logger. |
| 140 | func WithLogger(l *logger.Logger) Option { |
| 141 | return func(cfg *engineConfig) error { |
| 142 | cfg.log = l |
| 143 | return nil |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | // WithSeriesSelectionAllVisible configures planner scan to use all visible |
| 148 | // series from the reader (instead of only latest-success-seq series). |
| 149 | // This is intended for runtime/internal stores. |
| 150 | func WithSeriesSelectionAllVisible() Option { |
| 151 | return func(cfg *engineConfig) error { |
| 152 | cfg.seriesSelection = seriesSelectionAllVisible |
| 153 | return nil |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | // WithRuntimePlannerMode enables runtime/internal planner semantics for |
| 158 | // build-cycle dedupe bookkeeping while keeping lifecycle/cache tied to |
| 159 | // source LastSuccessSeq. |
| 160 | func WithRuntimePlannerMode() Option { |
| 161 | return func(cfg *engineConfig) error { |
| 162 | cfg.runtimePlanner = true |
| 163 | return nil |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | // WithEmitTypeIDBudgetPrefix configures chartengine autogen type-id budget |
| 168 | // checks to use the effective emission type-id prefix (for example job fullName). |
| 169 | func WithEmitTypeIDBudgetPrefix(typeID string) Option { |
| 170 | return func(cfg *engineConfig) error { |
| 171 | cfg.autogenTypeID = typeID |
| 172 | return nil |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | func applyOptions(opts ...Option) (engineConfig, error) { |
| 177 | cfg := engineConfig{ |
| 178 | autogen: defaultAutogenPolicy(), |
| 179 | seriesSelection: seriesSelectionLastSuccessOnly, |
| 180 | } |
| 181 | for i, opt := range opts { |
| 182 | if opt == nil { |
| 183 | continue |
| 184 | } |
| 185 | if err := opt(&cfg); err != nil { |
| 186 | return engineConfig{}, fmt.Errorf("chartengine: option[%d]: %w", i, err) |
| 187 | } |
| 188 | } |
| 189 | return cfg, nil |
| 190 | } |