master
go 1,744 lines 67 KB
Raw
1 // Unless explicitly stated otherwise all files in this repository are licensed
2 // under the Apache License Version 2.0.
3 // This product includes software developed at Datadog (https://www.datadoghq.com/).
4 // Copyright 2016-present Datadog, Inc.
5
6 package ddprofiledefinition
7
8 import (
9 "errors"
10 "fmt"
11 "regexp"
12 "slices"
13 "strconv"
14 "strings"
15 )
16
17 var validMetadataResources = map[string]map[string]bool{
18 "device": {
19 "name": true,
20 "description": true,
21 "sys_object_id": true,
22 "location": true,
23 "serial_number": true,
24 "vendor": true,
25 "version": true,
26 "software_version": true,
27 "firmware_version": true,
28 "hardware_version": true,
29 "product_name": true,
30 "model": true,
31 "os_name": true,
32 "os_version": true,
33 "os_hostname": true,
34 "category": true,
35 "type": true,
36 "lldp_loc_chassis_id": true,
37 "lldp_loc_chassis_id_subtype": true,
38 "lldp_loc_sys_name": true,
39 "lldp_loc_sys_desc": true,
40 "lldp_loc_sys_cap_supported": true,
41 "lldp_loc_sys_cap_enabled": true,
42 "bridge_base_address": true,
43 "stp_designated_root": true,
44 "vtp_version": true,
45 },
46 "interface": {
47 "name": true,
48 "alias": true,
49 "description": true,
50 "mac_address": true,
51 "admin_status": true,
52 "oper_status": true,
53 },
54 }
55
56 // SymbolContext represent the context in which the symbol is used
57 type SymbolContext int64
58
59 // ScalarSymbol enums
60 const (
61 ScalarSymbol SymbolContext = iota
62 ColumnSymbol
63 MetricTagSymbol
64 MetadataSymbol
65 TopologyScalarSymbol
66 TopologyColumnSymbol
67 )
68
69 // ValidateEnrichProfile validates a profile and normalizes it.
70 func ValidateEnrichProfile(p *ProfileDefinition) error {
71 normalizeMetrics(p.Metrics)
72 normalizeTopology(p.Topology)
73 normalizeLicensing(p.Licensing)
74 normalizeBGP(p.BGP)
75
76 errs := []error{
77 validateEnrichLegacySelector(p),
78 validateEnrichMetadata(p.Metadata),
79 validateEnrichSysobjectIDMetadata(p.SysobjectIDMetadata),
80 validateEnrichMetrics(p.Metrics),
81 validateEnrichTopology(p.Topology),
82 validateEnrichLicensing(p.Licensing),
83 validateEnrichBGP(p.BGP),
84 validateEnrichGlobalMetricTags(p.MetricTags),
85 validateEnrichVirtualMetrics(p.Metrics, p.Topology, p.VirtualMetrics),
86 }
87
88 return errors.Join(errs...)
89 }
90
91 // normalizeMetrics converts legacy syntax to new syntax
92 // 1/ converts old symbol syntax to new symbol syntax
93 // metric.Name and metric.OID info are moved to metric.Symbol.Name and metric.Symbol.OID
94 func normalizeMetrics(metrics []MetricsConfig) {
95 for i := range metrics {
96 normalizeMetric(&metrics[i])
97 }
98 }
99
100 func normalizeTopology(topology []TopologyConfig) {
101 for i := range topology {
102 normalizeMetric(&topology[i].MetricsConfig)
103 }
104 }
105
106 func normalizeLicensing(licensing []LicensingConfig) {
107 for i := range licensing {
108 normalizeLicenseValue(&licensing[i].Identity.ID)
109 normalizeLicenseValue(&licensing[i].Identity.Name)
110 normalizeLicenseValue(&licensing[i].Identity.Feature)
111 normalizeLicenseValue(&licensing[i].Identity.Component)
112 normalizeLicenseValue(&licensing[i].Descriptors.Type)
113 normalizeLicenseValue(&licensing[i].Descriptors.Impact)
114 normalizeLicenseValue(&licensing[i].Descriptors.Perpetual)
115 normalizeLicenseValue(&licensing[i].Descriptors.Unlimited)
116 normalizeLicenseValue(&licensing[i].State.LicenseValueConfig)
117 normalizeLicenseSignals(&licensing[i].Signals)
118 }
119 }
120
121 func normalizeBGP(rows []BGPConfig) {
122 for i := range rows {
123 normalizeBGPValue(&rows[i].Identity.RoutingInstance)
124 normalizeBGPValue(&rows[i].Identity.Neighbor)
125 normalizeBGPValue(&rows[i].Identity.RemoteAS)
126 normalizeBGPValue(&rows[i].Identity.AddressFamily.BGPValueConfig)
127 normalizeBGPValue(&rows[i].Identity.SubsequentAddressFamily.BGPValueConfig)
128 normalizeBGPValue(&rows[i].Descriptors.LocalAddress)
129 normalizeBGPValue(&rows[i].Descriptors.LocalAS)
130 normalizeBGPValue(&rows[i].Descriptors.LocalIdentifier)
131 normalizeBGPValue(&rows[i].Descriptors.PeerIdentifier)
132 normalizeBGPValue(&rows[i].Descriptors.PeerType)
133 normalizeBGPValue(&rows[i].Descriptors.BGPVersion)
134 normalizeBGPValue(&rows[i].Descriptors.Description)
135 forEachBGPSignalValueConfig(&rows[i], func(_ string, value *BGPValueConfig) {
136 normalizeBGPValue(value)
137 })
138 }
139 }
140
141 func normalizeBGPValue(value *BGPValueConfig) {
142 if value.Symbol.Name == "" && value.Symbol.OID == "" && value.Name != "" && value.OID != "" {
143 value.Symbol.Name = value.Name
144 value.Symbol.OID = value.OID
145 value.Name = ""
146 value.OID = ""
147 }
148 if value.Symbol.Format == "" {
149 value.Symbol.Format = value.Format
150 value.Format = ""
151 }
152 }
153
154 func normalizeLicenseSignals(signals *LicenseSignalsConfig) {
155 normalizeLicenseTimerSignals(&signals.Expiry)
156 normalizeLicenseTimerSignals(&signals.Authorization)
157 normalizeLicenseTimerSignals(&signals.Certificate)
158 normalizeLicenseTimerSignals(&signals.Grace)
159 normalizeLicenseValue(&signals.Usage.Used)
160 normalizeLicenseValue(&signals.Usage.Capacity)
161 normalizeLicenseValue(&signals.Usage.Available)
162 normalizeLicenseValue(&signals.Usage.Percent)
163 }
164
165 func normalizeLicenseTimerSignals(signals *LicenseTimerSignalsConfig) {
166 normalizeLicenseValue(&signals.LicenseValueConfig)
167 normalizeLicenseValue(&signals.Timestamp)
168 normalizeLicenseValue(&signals.Remaining)
169 }
170
171 func normalizeLicenseValue(value *LicenseValueConfig) {
172 if value.Symbol.Name == "" && value.Symbol.OID == "" && value.Name != "" && value.OID != "" {
173 value.Symbol.Name = value.Name
174 value.Symbol.OID = value.OID
175 value.Name = ""
176 value.OID = ""
177 }
178 if value.Symbol.Format == "" {
179 value.Symbol.Format = value.Format
180 value.Format = ""
181 }
182 }
183
184 func normalizeMetric(metric *MetricsConfig) {
185 if metric == nil {
186 return
187 }
188
189 // converts old symbol syntax to new symbol syntax
190 if metric.Symbol.Name == "" && metric.Symbol.OID == "" && metric.Name != "" && metric.OID != "" {
191 metric.Symbol.Name = metric.Name
192 metric.Symbol.OID = metric.OID
193 metric.Name = ""
194 metric.OID = ""
195 }
196 if metric.Symbol.MetricType == "" {
197 metric.Symbol.MetricType = metric.MetricType
198 metric.MetricType = ""
199 }
200 }
201
202 func validateEnrichLegacySelector(p *ProfileDefinition) error {
203 var errs []error
204
205 // If the new selector is absent but legacy sysobjectid exists, migrate it.
206 if len(p.Selector) == 0 && len(p.SysObjectIDs) > 0 {
207 p.Selector = SelectorSpec{
208 {
209 SysObjectID: SelectorIncludeExclude{
210 Include: slices.Clone(p.SysObjectIDs), // legacy -> include
211 },
212 },
213 }
214 }
215
216 // Normalize and validate every rule
217 for i := range p.Selector {
218 r := &p.Selector[i]
219
220 // Validate regex syntax for sysObjectID includes/excludes
221 for j, pat := range r.SysObjectID.Include {
222 if _, err := regexp.Compile(pat); err != nil {
223 errs = append(errs, fmt.Errorf("selector[%d].sysObjectID.include[%d]: invalid regex %q: %v", i, j, pat, err))
224 }
225 }
226 for j, pat := range r.SysObjectID.Exclude {
227 if _, err := regexp.Compile(pat); err != nil {
228 errs = append(errs, fmt.Errorf("selector[%d].sysObjectID.exclude[%d]: invalid regex %q: %v", i, j, pat, err))
229 }
230 }
231 }
232
233 return errors.Join(errs...)
234 }
235
236 func validateEnrichMetadata(metadata MetadataConfig) error {
237 var errs []error
238
239 for resName := range metadata {
240 _, isValidRes := validMetadataResources[resName]
241 if !isValidRes {
242 errs = append(errs, fmt.Errorf("invalid resource: %s", resName))
243 } else {
244 res := metadata[resName]
245 for fieldName := range res.Fields {
246 if !isValidMetadataField(resName, fieldName) {
247 errs = append(errs, fmt.Errorf("invalid resource (%s) field: %s", resName, fieldName))
248 continue
249 }
250 field := res.Fields[fieldName]
251 errs = append(errs, validateConsumers(fmt.Sprintf("metadata.%s.fields.%s.consumers", resName, fieldName), field.Consumers))
252 for i := range field.Symbols {
253 errs = append(errs, validateEnrichSymbol(&field.Symbols[i], MetadataSymbol))
254 }
255 if field.Symbol.OID != "" {
256 errs = append(errs, validateEnrichSymbol(&field.Symbol, MetadataSymbol))
257 }
258 res.Fields[fieldName] = field
259 }
260 metadata[resName] = res
261 }
262 if resName == "device" && len(metadata[resName].IDTags) > 0 {
263 errs = append(errs, errors.New("device resource does not support custom id_tags"))
264 }
265 for i := range metadata[resName].IDTags {
266 metricTag := &metadata[resName].IDTags[i]
267 errs = append(errs, validateEnrichMetricTag(metricTag))
268 }
269 }
270
271 return errors.Join(errs...)
272 }
273
274 func validateEnrichSysobjectIDMetadata(entries []SysobjectIDMetadataEntryConfig) error {
275 var errs []error
276
277 // Track seen sysobjectids to detect duplicates
278 seenOIDs := make(map[string]int) // OID -> first occurrence index
279
280 for i, entry := range entries {
281 // Validate sysobjectid is not empty
282 if entry.SysobjectID == "" {
283 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d]: missing sysobjectid", i))
284 continue
285 }
286
287 // Check for duplicate sysobjectids
288 if firstIdx, exists := seenOIDs[entry.SysobjectID]; exists {
289 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d]: duplicate sysobjectid %s (first occurrence at index %d)",
290 i, entry.SysobjectID, firstIdx))
291 } else {
292 seenOIDs[entry.SysobjectID] = i
293 }
294
295 // Validate metadata fields
296 for fieldName, field := range entry.Metadata {
297 if !isValidMetadataField(MetadataDeviceResource, fieldName) {
298 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d]: invalid resource (%s) field: %s", i, MetadataDeviceResource, fieldName))
299 continue
300 }
301
302 // Validate the field must have either value or symbol(s)
303 if field.Value == "" && field.Symbol.OID == "" && len(field.Symbols) == 0 {
304 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d].%s: must have either value or symbol(s)", i, fieldName))
305 }
306 errs = append(errs, validateConsumers(fmt.Sprintf("sysobjectid_metadata[%d].%s.consumers", i, fieldName), field.Consumers))
307
308 // Can't have both value and symbols
309 if field.Value != "" && (field.Symbol.OID != "" || len(field.Symbols) > 0) {
310 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d].%s: cannot have both value and symbol(s)", i, fieldName))
311 }
312
313 // Validate symbols if present
314 for j := range field.Symbols {
315 if err := validateEnrichSymbol(&field.Symbols[j], MetadataSymbol); err != nil {
316 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d].%s.symbols[%d]: %s", i, fieldName, j, err))
317 }
318 }
319
320 // Validate single symbol if present
321 if field.Symbol.OID != "" {
322 if err := validateEnrichSymbol(&field.Symbol, MetadataSymbol); err != nil {
323 errs = append(errs, fmt.Errorf("sysobjectid_metadata[%d].%s.symbol: %s", i, fieldName, err))
324 }
325 }
326
327 entry.Metadata[fieldName] = field
328 }
329 }
330
331 return errors.Join(errs...)
332 }
333
334 func isValidMetadataField(resourceName, fieldName string) bool {
335 fields, ok := validMetadataResources[resourceName]
336 if !ok {
337 return false
338 }
339 _, ok = fields[fieldName]
340 return ok
341 }
342
343 func validateEnrichMetrics(metrics []MetricsConfig) error {
344 var errs []error
345
346 for i := range metrics {
347 metricConfig := &metrics[i]
348 if !metricConfig.IsScalar() && !metricConfig.IsColumn() {
349 errs = append(errs, fmt.Errorf("either a table symbol or a scalar symbol must be provided: %#v", metricConfig))
350 }
351 if metricConfig.IsScalar() && metricConfig.IsColumn() {
352 errs = append(errs, fmt.Errorf("table symbol and scalar symbol cannot be both provided: %#v", metricConfig))
353 }
354 if metricConfig.IsScalar() {
355 errs = append(errs, validateEnrichSymbol(&metricConfig.Symbol, ScalarSymbol))
356 for j := range metricConfig.MetricTags {
357 metricTag := &metricConfig.MetricTags[j]
358 errs = append(errs, validateEnrichMetricTag(metricTag))
359 if metricTag.Table != "" {
360 errs = append(errs, fmt.Errorf("scalar metric_tags do not support `table` lookups (tag=%q, table=%q)", metricTag.Tag, metricTag.Table))
361 }
362 if metricTag.Index != 0 {
363 errs = append(errs, fmt.Errorf("scalar metric_tags do not support `index` lookups (tag=%q, index=%d)", metricTag.Tag, metricTag.Index))
364 }
365 if len(metricTag.IndexTransform) > 0 {
366 errs = append(errs, fmt.Errorf("scalar metric_tags do not support `index_transform` (tag=%q)", metricTag.Tag))
367 }
368 if metricTag.Symbol.OID == "" {
369 errs = append(errs, fmt.Errorf("scalar metric_tags require `symbol.OID` (tag=%q)", metricTag.Tag))
370 }
371 }
372 }
373 if metricConfig.IsColumn() {
374 for j := range metricConfig.Symbols {
375 errs = append(errs, validateEnrichSymbol(&metricConfig.Symbols[j], ColumnSymbol))
376 }
377 if len(metricConfig.MetricTags) == 0 {
378 errs = append(errs, fmt.Errorf("column symbols doesn't have a 'metric_tags' section (%+v), all its metrics will use the same tags; "+
379 "if the table has multiple rows, only one row will be submitted; "+
380 "please add at least one discriminating metric tag (such as a row index) "+
381 "to ensure metrics of all rows are submitted", metricConfig.Symbols))
382 }
383 for i := range metricConfig.MetricTags {
384 metricTag := &metricConfig.MetricTags[i]
385 errs = append(errs, validateEnrichMetricTag(metricTag))
386 }
387 }
388 }
389
390 return errors.Join(errs...)
391 }
392
393 func validateEnrichTopology(topology []TopologyConfig) error {
394 var errs []error
395
396 for i := range topology {
397 topo := &topology[i]
398 metricConfig := &topo.MetricsConfig
399
400 if topo.Kind == "" {
401 errs = append(errs, fmt.Errorf("topology[%d]: missing kind", i))
402 } else if !IsValidTopologyKind(topo.Kind) {
403 errs = append(errs, fmt.Errorf("topology[%d]: invalid kind %q", i, topo.Kind))
404 }
405 if !metricConfig.IsScalar() && !metricConfig.IsColumn() {
406 errs = append(errs, fmt.Errorf("topology[%d]: either a table symbol or a scalar symbol must be provided: %#v", i, metricConfig))
407 }
408 if metricConfig.IsScalar() && metricConfig.IsColumn() {
409 errs = append(errs, fmt.Errorf("topology[%d]: table symbol and scalar symbol cannot be both provided: %#v", i, metricConfig))
410 }
411 if metricConfig.Options != (MetricsConfigOption{}) {
412 errs = append(errs, fmt.Errorf("topology[%d]: options cannot be used in topology rows", i))
413 }
414 if metricConfig.IsScalar() {
415 errs = append(errs, validateEnrichTopologySymbol(i, &metricConfig.Symbol, TopologyScalarSymbol))
416 for j := range metricConfig.MetricTags {
417 metricTag := &metricConfig.MetricTags[j]
418 errs = append(errs, validateEnrichMetricTag(metricTag))
419 if metricTag.Table != "" {
420 errs = append(errs, fmt.Errorf("topology[%d].metric_tags[%d]: scalar metric_tags do not support `table` lookups (tag=%q, table=%q)", i, j, metricTag.Tag, metricTag.Table))
421 }
422 if metricTag.Index != 0 {
423 errs = append(errs, fmt.Errorf("topology[%d].metric_tags[%d]: scalar metric_tags do not support `index` lookups (tag=%q, index=%d)", i, j, metricTag.Tag, metricTag.Index))
424 }
425 if len(metricTag.IndexTransform) > 0 {
426 errs = append(errs, fmt.Errorf("topology[%d].metric_tags[%d]: scalar metric_tags do not support `index_transform` (tag=%q)", i, j, metricTag.Tag))
427 }
428 if metricTag.Symbol.OID == "" {
429 errs = append(errs, fmt.Errorf("topology[%d].metric_tags[%d]: scalar metric_tags require `symbol.OID` (tag=%q)", i, j, metricTag.Tag))
430 }
431 }
432 }
433 if metricConfig.IsColumn() {
434 for j := range metricConfig.Symbols {
435 errs = append(errs, validateEnrichTopologySymbol(i, &metricConfig.Symbols[j], TopologyColumnSymbol))
436 }
437 for j := range metricConfig.MetricTags {
438 errs = append(errs, validateEnrichMetricTag(&metricConfig.MetricTags[j]))
439 }
440 }
441 }
442
443 return errors.Join(errs...)
444 }
445
446 func validateEnrichLicensing(licensing []LicensingConfig) error {
447 var errs []error
448 seenSignals := make(map[licenseSignalValidationKey]string)
449
450 for i := range licensing {
451 row := &licensing[i]
452 isTable := row.Table.OID != ""
453 errs = append(errs, validateEnrichLicenseRowShape(i, row))
454 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].identity.id", i), &row.Identity.ID, isTable))
455 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].identity.name", i), &row.Identity.Name, isTable))
456 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].identity.feature", i), &row.Identity.Feature, isTable))
457 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].identity.component", i), &row.Identity.Component, isTable))
458 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].descriptors.type", i), &row.Descriptors.Type, isTable))
459 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].descriptors.impact", i), &row.Descriptors.Impact, isTable))
460 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].descriptors.perpetual", i), &row.Descriptors.Perpetual, isTable))
461 errs = append(errs, validateEnrichLicenseValue(fmt.Sprintf("licensing[%d].descriptors.unlimited", i), &row.Descriptors.Unlimited, isTable))
462 errs = append(errs, validateEnrichLicenseState(i, &row.State, isTable))
463 errs = append(errs, validateEnrichLicenseSignals(i, &row.Signals, isTable))
464 errs = append(errs, validateLicenseFromReferences(i, row))
465 errs = append(errs, validateLicenseSignalDuplicates(i, row, seenSignals))
466 for j := range row.MetricTags {
467 errs = append(errs, validateEnrichMetricTag(&row.MetricTags[j]))
468 if !isTable {
469 metricTag := &row.MetricTags[j]
470 if metricTag.Table != "" {
471 errs = append(errs, fmt.Errorf("licensing[%d].metric_tags[%d]: scalar metric_tags do not support `table` lookups (tag=%q, table=%q)", i, j, metricTag.Tag, metricTag.Table))
472 }
473 if metricTag.Index != 0 {
474 errs = append(errs, fmt.Errorf("licensing[%d].metric_tags[%d]: scalar metric_tags do not support `index` lookups (tag=%q, index=%d)", i, j, metricTag.Tag, metricTag.Index))
475 }
476 if len(metricTag.IndexTransform) > 0 {
477 errs = append(errs, fmt.Errorf("licensing[%d].metric_tags[%d]: scalar metric_tags do not support `index_transform` (tag=%q)", i, j, metricTag.Tag))
478 }
479 if metricTag.Symbol.OID == "" {
480 errs = append(errs, fmt.Errorf("licensing[%d].metric_tags[%d]: scalar metric_tags require `symbol.OID` (tag=%q)", i, j, metricTag.Tag))
481 }
482 }
483 }
484 }
485
486 return errors.Join(errs...)
487 }
488
489 func validateEnrichLicenseRowShape(rowIdx int, row *LicensingConfig) error {
490 var errs []error
491
492 if row.Table.Name != "" && row.Table.OID == "" {
493 errs = append(errs, fmt.Errorf("licensing[%d].table: table name %q requires table OID", rowIdx, row.Table.Name))
494 }
495 if row.Table.OID != "" && row.Table.Name == "" {
496 errs = append(errs, fmt.Errorf("licensing[%d].table: table OID %q requires table name", rowIdx, row.Table.OID))
497 }
498 if !licenseRowHasSignalConfigs(*row) {
499 errs = append(errs, fmt.Errorf("licensing[%d]: must define state or at least one signal", rowIdx))
500 }
501 if row.Table.OID == "" && row.ID == "" {
502 sourceOIDs := collectLicenseSignalSourceOIDs(*row)
503 switch {
504 case len(sourceOIDs) == 0:
505 errs = append(errs, fmt.Errorf("licensing[%d]: scalar rows without a signal source OID require explicit id", rowIdx))
506 case len(sourceOIDs) > 1:
507 errs = append(errs, fmt.Errorf("licensing[%d]: scalar rows with multiple signal source OIDs require explicit id", rowIdx))
508 }
509 }
510
511 return errors.Join(errs...)
512 }
513
514 func validateEnrichLicenseState(rowIdx int, state *LicenseStateConfig, isTable bool) error {
515 var errs []error
516 if state.Policy != "" && !IsValidLicenseStatePolicy(state.Policy) {
517 errs = append(errs, fmt.Errorf("licensing[%d].state.policy: invalid policy %q", rowIdx, state.Policy))
518 }
519 if state.Policy != "" && !state.LicenseValueConfig.IsSet() {
520 errs = append(errs, fmt.Errorf("licensing[%d].state.policy: policy requires state value source", rowIdx))
521 }
522 errs = append(errs, validateEnrichLicenseValueKind(fmt.Sprintf("licensing[%d].state", rowIdx), &state.LicenseValueConfig, LicenseSignalStateSeverity, isTable))
523 return errors.Join(errs...)
524 }
525
526 func validateEnrichLicenseSignals(rowIdx int, signals *LicenseSignalsConfig, isTable bool) error {
527 var errs []error
528 errs = append(errs, validateEnrichLicenseTimerSignals(rowIdx, "expiry", &signals.Expiry, LicenseSignalExpiryTimestamp, LicenseSignalExpiryRemaining, isTable))
529 errs = append(errs, validateEnrichLicenseTimerSignals(rowIdx, "authorization", &signals.Authorization, LicenseSignalAuthorizationTimestamp, LicenseSignalAuthorizationRemaining, isTable))
530 errs = append(errs, validateEnrichLicenseTimerSignals(rowIdx, "certificate", &signals.Certificate, LicenseSignalCertificateTimestamp, LicenseSignalCertificateRemaining, isTable))
531 errs = append(errs, validateEnrichLicenseTimerSignals(rowIdx, "grace", &signals.Grace, LicenseSignalGraceTimestamp, LicenseSignalGraceRemaining, isTable))
532 errs = append(errs, validateEnrichLicenseValueKind(fmt.Sprintf("licensing[%d].signals.usage.used", rowIdx), &signals.Usage.Used, LicenseSignalUsageUsed, isTable))
533 errs = append(errs, validateEnrichLicenseValueKind(fmt.Sprintf("licensing[%d].signals.usage.capacity", rowIdx), &signals.Usage.Capacity, LicenseSignalUsageCapacity, isTable))
534 errs = append(errs, validateEnrichLicenseValueKind(fmt.Sprintf("licensing[%d].signals.usage.available", rowIdx), &signals.Usage.Available, LicenseSignalUsageAvailable, isTable))
535 errs = append(errs, validateEnrichLicenseValueKind(fmt.Sprintf("licensing[%d].signals.usage.percent", rowIdx), &signals.Usage.Percent, LicenseSignalUsagePercent, isTable))
536 return errors.Join(errs...)
537 }
538
539 func validateEnrichLicenseTimerSignals(rowIdx int, name string, signals *LicenseTimerSignalsConfig, timestampKind, remainingKind LicenseSignalKind, isTable bool) error {
540 var errs []error
541 basePath := fmt.Sprintf("licensing[%d].signals.%s", rowIdx, name)
542 if signals.LicenseValueConfig.IsSet() && signals.Timestamp.IsSet() {
543 errs = append(errs, fmt.Errorf("%s: inline timestamp and timestamp cannot both be set", basePath))
544 }
545 if (signals.LicenseValueConfig.IsSet() || signals.Timestamp.IsSet()) && signals.Remaining.IsSet() {
546 errs = append(errs, fmt.Errorf("%s: timestamp and remaining cannot both be set", basePath))
547 }
548 errs = append(errs, validateEnrichLicenseValueKind(basePath, &signals.LicenseValueConfig, timestampKind, isTable))
549 errs = append(errs, validateEnrichLicenseValueKind(basePath+".timestamp", &signals.Timestamp, timestampKind, isTable))
550 errs = append(errs, validateEnrichLicenseValueKind(basePath+".remaining", &signals.Remaining, remainingKind, isTable))
551 return errors.Join(errs...)
552 }
553
554 func validateEnrichLicenseValueKind(path string, value *LicenseValueConfig, defaultKind LicenseSignalKind, isTable bool) error {
555 if !value.IsSet() {
556 return nil
557 }
558 if value.Kind == "" {
559 value.Kind = defaultKind
560 } else if value.Kind != defaultKind {
561 if !IsValidLicenseSignalKind(value.Kind) {
562 return fmt.Errorf("%s.kind: invalid kind %q", path, value.Kind)
563 }
564 return fmt.Errorf("%s.kind: expected %q, got %q", path, defaultKind, value.Kind)
565 }
566 return validateEnrichLicenseValue(path, value, isTable)
567 }
568
569 func validateEnrichLicenseValue(path string, value *LicenseValueConfig, isTable bool) error {
570 var errs []error
571 if !value.IsSet() {
572 return nil
573 }
574
575 if value.Kind != "" && !IsValidLicenseSignalKind(value.Kind) {
576 errs = append(errs, fmt.Errorf("%s.kind: invalid kind %q", path, value.Kind))
577 }
578 if !licenseValueHasSource(*value) {
579 errs = append(errs, fmt.Errorf("%s: must define value, from, symbol.OID, OID, index, or index_transform", path))
580 }
581 for i, policy := range value.Sentinel {
582 if !IsValidLicenseSentinelPolicy(policy) {
583 errs = append(errs, fmt.Errorf("%s.sentinel[%d]: invalid policy %q", path, i, policy))
584 }
585 }
586 errs = append(errs, validateMapping(value.Mapping, MetadataSymbol))
587
588 if strings.HasPrefix(value.Name, "_") {
589 errs = append(errs, fmt.Errorf("%s.name: name %q cannot be underscore-prefixed", path, value.Name))
590 }
591 if !isTable {
592 if value.Index != 0 {
593 errs = append(errs, fmt.Errorf("%s.index: scalar licensing values do not support `index` lookups", path))
594 }
595 if len(value.IndexTransform) > 0 {
596 errs = append(errs, fmt.Errorf("%s.index_transform: scalar licensing values do not support `index_transform`", path))
597 }
598 }
599 if value.Format != "" && !isValidLicenseValueFormat(value.Format) {
600 errs = append(errs, fmt.Errorf("%s.format: invalid format %q", path, value.Format))
601 }
602 if value.Symbol.Format != "" && !isValidLicenseValueFormat(value.Symbol.Format) {
603 errs = append(errs, fmt.Errorf("%s.symbol: invalid format %q", path, value.Symbol.Format))
604 }
605 if value.Symbol.OID != "" || value.Symbol.Name != "" {
606 errs = append(errs, validateEnrichLicenseSymbol(path, &value.Symbol))
607 }
608
609 return errors.Join(errs...)
610 }
611
612 func validateEnrichLicenseSymbol(path string, symbol *SymbolConfig) error {
613 var errs []error
614
615 errs = append(errs, validateEnrichSymbol(symbol, MetadataSymbol))
616 if strings.HasPrefix(symbol.Name, "_") {
617 errs = append(errs, fmt.Errorf("%s.symbol: name %q cannot be underscore-prefixed", path, symbol.Name))
618 }
619 if symbol.ChartMeta != (ChartMeta{}) {
620 errs = append(errs, fmt.Errorf("%s.symbol: chart_meta cannot be used in licensing rows", path))
621 }
622 if symbol.MetricType != "" {
623 errs = append(errs, fmt.Errorf("%s.symbol: metric_type cannot be used in licensing rows", path))
624 }
625 if symbol.Transform != "" {
626 errs = append(errs, fmt.Errorf("%s.symbol: transform cannot be used in licensing rows", path))
627 }
628 if symbol.ExtractValue != "" {
629 errs = append(errs, fmt.Errorf("%s.symbol: extract_value cannot be used in licensing rows", path))
630 }
631 if symbol.MatchPattern != "" {
632 errs = append(errs, fmt.Errorf("%s.symbol: match_pattern cannot be used in licensing rows", path))
633 }
634 if symbol.MatchValue != "" {
635 errs = append(errs, fmt.Errorf("%s.symbol: match_value cannot be used in licensing rows", path))
636 }
637 if symbol.ScaleFactor != 0 {
638 errs = append(errs, fmt.Errorf("%s.symbol: scale_factor cannot be used in licensing rows", path))
639 }
640 if symbol.ConstantValueOne {
641 errs = append(errs, fmt.Errorf("%s.symbol: constant_value_one cannot be used in licensing rows", path))
642 }
643
644 return errors.Join(errs...)
645 }
646
647 var validLicenseValueFormats = map[string]struct{}{
648 "hex": {},
649 "ip_address": {},
650 "mac_address": {},
651 "snmp_dateandtime": {},
652 "text_date": {},
653 }
654
655 func isValidLicenseValueFormat(format string) bool {
656 _, ok := validLicenseValueFormats[format]
657 return ok
658 }
659
660 type licenseSignalValidationKey struct {
661 identity string
662 kind LicenseSignalKind
663 }
664
665 func validateLicenseSignalDuplicates(rowIdx int, row *LicensingConfig, seen map[licenseSignalValidationKey]string) error {
666 var errs []error
667 identity := LicenseStructuralIdentity(*row)
668 for _, sig := range collectLicenseSignalValues(*row) {
669 if sig.kind == "" || !sig.value.IsSet() {
670 continue
671 }
672 path := fmt.Sprintf("licensing[%d].%s", rowIdx, sig.path)
673 key := licenseSignalValidationKey{identity: identity, kind: sig.kind}
674 if prev, ok := seen[key]; ok {
675 errs = append(errs, fmt.Errorf("%s: duplicate signal kind %q for structural identity %q (first seen at %s)", path, sig.kind, identity, prev))
676 continue
677 }
678 seen[key] = path
679 }
680 return errors.Join(errs...)
681 }
682
683 func validateLicenseFromReferences(rowIdx int, row *LicensingConfig) error {
684 if row.Table.OID == "" {
685 return nil
686 }
687
688 var errs []error
689 tableOID := TrimLicenseOID(row.Table.OID)
690 for _, ref := range collectLicenseValueReferences(*row) {
691 if ref.value.From == "" {
692 continue
693 }
694 fromOID := TrimLicenseOID(ref.value.From)
695 if !oidHasPrefix(fromOID, tableOID) {
696 errs = append(errs, fmt.Errorf("licensing[%d].%s.from: OID %q is outside table %q", rowIdx, ref.path, ref.value.From, row.Table.OID))
697 }
698 }
699 return errors.Join(errs...)
700 }
701
702 type licenseValueValidationRef struct {
703 path string
704 value LicenseValueConfig
705 }
706
707 type licenseSignalValueValidationRef struct {
708 path string
709 kind LicenseSignalKind
710 value LicenseValueConfig
711 }
712
713 func collectLicenseSignalValues(row LicensingConfig) []licenseSignalValueValidationRef {
714 var values []licenseSignalValueValidationRef
715 add := func(path string, value LicenseValueConfig) {
716 if value.IsSet() {
717 values = append(values, licenseSignalValueValidationRef{path: path, kind: value.Kind, value: value})
718 }
719 }
720 add("state", row.State.LicenseValueConfig)
721 collectLicenseTimerSignalValues("signals.expiry", row.Signals.Expiry, add)
722 collectLicenseTimerSignalValues("signals.authorization", row.Signals.Authorization, add)
723 collectLicenseTimerSignalValues("signals.certificate", row.Signals.Certificate, add)
724 collectLicenseTimerSignalValues("signals.grace", row.Signals.Grace, add)
725 add("signals.usage.used", row.Signals.Usage.Used)
726 add("signals.usage.capacity", row.Signals.Usage.Capacity)
727 add("signals.usage.available", row.Signals.Usage.Available)
728 add("signals.usage.percent", row.Signals.Usage.Percent)
729 return values
730 }
731
732 func collectLicenseTimerSignalValues(path string, cfg LicenseTimerSignalsConfig, add func(string, LicenseValueConfig)) {
733 add(path, cfg.LicenseValueConfig)
734 add(path+".timestamp", cfg.Timestamp)
735 add(path+".remaining", cfg.Remaining)
736 }
737
738 func collectLicenseValueReferences(row LicensingConfig) []licenseValueValidationRef {
739 var values []licenseValueValidationRef
740 add := func(path string, value LicenseValueConfig) {
741 if value.IsSet() {
742 values = append(values, licenseValueValidationRef{path: path, value: value})
743 }
744 }
745 add("identity.id", row.Identity.ID)
746 add("identity.name", row.Identity.Name)
747 add("identity.feature", row.Identity.Feature)
748 add("identity.component", row.Identity.Component)
749 add("descriptors.type", row.Descriptors.Type)
750 add("descriptors.impact", row.Descriptors.Impact)
751 add("descriptors.perpetual", row.Descriptors.Perpetual)
752 add("descriptors.unlimited", row.Descriptors.Unlimited)
753 add("state", row.State.LicenseValueConfig)
754 collectLicenseTimerSignalValues("signals.expiry", row.Signals.Expiry, add)
755 collectLicenseTimerSignalValues("signals.authorization", row.Signals.Authorization, add)
756 collectLicenseTimerSignalValues("signals.certificate", row.Signals.Certificate, add)
757 collectLicenseTimerSignalValues("signals.grace", row.Signals.Grace, add)
758 add("signals.usage.used", row.Signals.Usage.Used)
759 add("signals.usage.capacity", row.Signals.Usage.Capacity)
760 add("signals.usage.available", row.Signals.Usage.Available)
761 add("signals.usage.percent", row.Signals.Usage.Percent)
762 return values
763 }
764
765 func licenseRowHasSignalConfigs(row LicensingConfig) bool {
766 if row.State.LicenseValueConfig.IsSet() {
767 return true
768 }
769 for _, sig := range collectLicenseSignalValues(row) {
770 if sig.value.IsSet() {
771 return true
772 }
773 }
774 return false
775 }
776
777 func collectLicenseSignalSourceOIDs(row LicensingConfig) map[string]struct{} {
778 oids := make(map[string]struct{})
779 for _, sig := range collectLicenseSignalValues(row) {
780 if oid := LicenseValueSourceOID(sig.value); oid != "" {
781 oids[TrimLicenseOID(oid)] = struct{}{}
782 }
783 }
784 return oids
785 }
786
787 func licenseValueHasSource(value LicenseValueConfig) bool {
788 return value.Value != "" ||
789 value.From != "" ||
790 value.Symbol.OID != "" ||
791 value.OID != "" ||
792 value.Index != 0 ||
793 len(value.IndexTransform) > 0
794 }
795
796 func oidHasPrefix(oid, prefix string) bool {
797 return oid == prefix || strings.HasPrefix(oid, prefix+".")
798 }
799
800 func validateEnrichBGP(rows []BGPConfig) error {
801 var errs []error
802 seenSignals := make(map[bgpSignalValidationKey]string)
803
804 for i := range rows {
805 row := &rows[i]
806 isTable := row.Table.OID != ""
807
808 errs = append(errs, validateEnrichBGPRowShape(i, row))
809 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].identity.routing_instance", i), &row.Identity.RoutingInstance, isTable))
810 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].identity.neighbor", i), &row.Identity.Neighbor, isTable))
811 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].identity.remote_as", i), &row.Identity.RemoteAS, isTable))
812 errs = append(errs, validateEnrichBGPAddressFamilyValue(fmt.Sprintf("bgp[%d].identity.address_family", i), &row.Identity.AddressFamily, isTable))
813 errs = append(errs, validateEnrichBGPSubsequentAddressFamilyValue(fmt.Sprintf("bgp[%d].identity.subsequent_address_family", i), &row.Identity.SubsequentAddressFamily, isTable))
814 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.local_address", i), &row.Descriptors.LocalAddress, isTable))
815 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.local_as", i), &row.Descriptors.LocalAS, isTable))
816 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.local_identifier", i), &row.Descriptors.LocalIdentifier, isTable))
817 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.peer_identifier", i), &row.Descriptors.PeerIdentifier, isTable))
818 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.peer_type", i), &row.Descriptors.PeerType, isTable))
819 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.bgp_version", i), &row.Descriptors.BGPVersion, isTable))
820 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].descriptors.description", i), &row.Descriptors.Description, isTable))
821 errs = append(errs, validateEnrichBGPSignalValues(i, row, isTable))
822 errs = append(errs, validateBGPGroupKindCompatibility(i, row))
823 errs = append(errs, validateBGPFromReferences(i, row))
824 errs = append(errs, validateBGPSignalDuplicates(i, row, seenSignals))
825
826 for j := range row.MetricTags {
827 errs = append(errs, validateEnrichMetricTag(&row.MetricTags[j]))
828 if !isTable {
829 metricTag := &row.MetricTags[j]
830 if metricTag.Table != "" {
831 errs = append(errs, fmt.Errorf("bgp[%d].metric_tags[%d]: scalar metric_tags do not support `table` lookups (tag=%q, table=%q)", i, j, metricTag.Tag, metricTag.Table))
832 }
833 if metricTag.Index != 0 {
834 errs = append(errs, fmt.Errorf("bgp[%d].metric_tags[%d]: scalar metric_tags do not support `index` lookups (tag=%q, index=%d)", i, j, metricTag.Tag, metricTag.Index))
835 }
836 if len(metricTag.IndexTransform) > 0 {
837 errs = append(errs, fmt.Errorf("bgp[%d].metric_tags[%d]: scalar metric_tags do not support `index_transform` (tag=%q)", i, j, metricTag.Tag))
838 }
839 if metricTag.Symbol.OID == "" {
840 errs = append(errs, fmt.Errorf("bgp[%d].metric_tags[%d]: scalar metric_tags require `symbol.OID` (tag=%q)", i, j, metricTag.Tag))
841 }
842 }
843 }
844 }
845
846 errs = append(errs, validateBGPTableReferences(rows))
847
848 return errors.Join(errs...)
849 }
850
851 func forEachBGPSignalValueConfig(row *BGPConfig, add func(path string, value *BGPValueConfig)) {
852 addState := func(path string, value *BGPStateConfig) {
853 add(path, &value.BGPValueConfig)
854 }
855 addDirectional := func(prefix string, value *BGPDirectionalConfig) {
856 add(prefix+".received", &value.Received)
857 add(prefix+".sent", &value.Sent)
858 }
859 addTimerPair := func(prefix string, value *BGPTimerPairConfig) {
860 add(prefix+".connect_retry", &value.ConnectRetry)
861 add(prefix+".hold_time", &value.HoldTime)
862 add(prefix+".keepalive_time", &value.KeepaliveTime)
863 add(prefix+".min_as_origination_interval", &value.MinASOriginationInterval)
864 add(prefix+".min_route_advertisement_interval", &value.MinRouteAdvertisementInterval)
865 }
866 addNotification := func(prefix string, value *BGPLastNotificationConfig) {
867 add(prefix+".code", &value.Code)
868 add(prefix+".subcode", &value.Subcode)
869 add(prefix+".reason", &value.Reason)
870 }
871 addRoutes := func(prefix string, value *BGPRouteCountersConfig) {
872 add(prefix+".received", &value.Received)
873 add(prefix+".accepted", &value.Accepted)
874 add(prefix+".rejected", &value.Rejected)
875 add(prefix+".active", &value.Active)
876 add(prefix+".advertised", &value.Advertised)
877 add(prefix+".suppressed", &value.Suppressed)
878 add(prefix+".withdrawn", &value.Withdrawn)
879 }
880
881 add("admin.enabled", &row.Admin.Enabled)
882 addState("state", &row.State)
883 addState("previous_state", &row.Previous)
884 add("connection.established_uptime", &row.Connection.EstablishedUptime)
885 add("connection.last_received_update_age", &row.Connection.LastReceivedUpdateAge)
886 addDirectional("traffic.messages", &row.Traffic.Messages)
887 addDirectional("traffic.updates", &row.Traffic.Updates)
888 addDirectional("traffic.notifications", &row.Traffic.Notifications)
889 addDirectional("traffic.route_refreshes", &row.Traffic.RouteRefreshes)
890 addDirectional("traffic.opens", &row.Traffic.Opens)
891 addDirectional("traffic.keepalives", &row.Traffic.Keepalives)
892 add("transitions.established", &row.Transitions.Established)
893 add("transitions.down", &row.Transitions.Down)
894 add("transitions.up", &row.Transitions.Up)
895 add("transitions.flaps", &row.Transitions.Flaps)
896 addTimerPair("timers.negotiated", &row.Timers.Negotiated)
897 addTimerPair("timers.configured", &row.Timers.Configured)
898 add("last_error.code", &row.LastError.Code)
899 add("last_error.subcode", &row.LastError.Subcode)
900 addNotification("last_notifications.received", &row.LastNotify.Received)
901 addNotification("last_notifications.sent", &row.LastNotify.Sent)
902 add("reasons.last_down", &row.Reasons.LastDown)
903 add("reasons.unavailability", &row.Reasons.Unavailability)
904 add("graceful_restart.state", &row.Restart.State)
905 addRoutes("routes.current", &row.Routes.Current)
906 addRoutes("routes.total", &row.Routes.Total)
907 add("route_limits.limit", &row.RouteLimits.Limit)
908 add("route_limits.threshold", &row.RouteLimits.Threshold)
909 add("route_limits.clear_threshold", &row.RouteLimits.ClearThreshold)
910 add("device_counts.peers", &row.Device.Peers)
911 add("device_counts.ibgp_peers", &row.Device.InternalPeers)
912 add("device_counts.ebgp_peers", &row.Device.ExternalPeers)
913 add("device_counts.states.idle", &row.Device.States.Idle)
914 add("device_counts.states.connect", &row.Device.States.Connect)
915 add("device_counts.states.active", &row.Device.States.Active)
916 add("device_counts.states.opensent", &row.Device.States.OpenSent)
917 add("device_counts.states.openconfirm", &row.Device.States.OpenConfirm)
918 add("device_counts.states.established", &row.Device.States.Established)
919 }
920
921 func validateEnrichBGPRowShape(rowIdx int, row *BGPConfig) error {
922 var errs []error
923
924 if row.Kind == "" {
925 errs = append(errs, fmt.Errorf("bgp[%d].kind: missing kind", rowIdx))
926 } else if !IsValidBGPRowKind(row.Kind) {
927 errs = append(errs, fmt.Errorf("bgp[%d].kind: invalid kind %q", rowIdx, row.Kind))
928 }
929 if row.Table.Name != "" && row.Table.OID == "" {
930 errs = append(errs, fmt.Errorf("bgp[%d].table: table name %q requires table OID", rowIdx, row.Table.Name))
931 }
932 if row.Table.OID != "" && row.Table.Name == "" {
933 errs = append(errs, fmt.Errorf("bgp[%d].table: table OID %q requires table name", rowIdx, row.Table.OID))
934 }
935 if !bgpRowHasSignalConfigs(*row) {
936 errs = append(errs, fmt.Errorf("bgp[%d]: must define at least one typed BGP field", rowIdx))
937 }
938
939 switch row.Kind {
940 case BGPRowKindPeer:
941 if !row.Identity.Neighbor.IsSet() {
942 errs = append(errs, fmt.Errorf("bgp[%d].identity.neighbor: peer rows require neighbor identity", rowIdx))
943 }
944 if !row.Identity.RemoteAS.IsSet() {
945 errs = append(errs, fmt.Errorf("bgp[%d].identity.remote_as: peer rows require remote_as identity", rowIdx))
946 }
947 case BGPRowKindPeerFamily:
948 if !row.Identity.Neighbor.IsSet() {
949 errs = append(errs, fmt.Errorf("bgp[%d].identity.neighbor: peer_family rows require neighbor identity", rowIdx))
950 }
951 if !row.Identity.RemoteAS.IsSet() {
952 errs = append(errs, fmt.Errorf("bgp[%d].identity.remote_as: peer_family rows require remote_as identity", rowIdx))
953 }
954 if !row.Identity.AddressFamily.IsSet() {
955 errs = append(errs, fmt.Errorf("bgp[%d].identity.address_family: peer_family rows require address_family identity", rowIdx))
956 }
957 if !row.Identity.SubsequentAddressFamily.IsSet() {
958 errs = append(errs, fmt.Errorf("bgp[%d].identity.subsequent_address_family: peer_family rows require subsequent_address_family identity", rowIdx))
959 }
960 }
961
962 if row.Table.OID == "" && row.ID == "" {
963 sourceOIDs := collectBGPSignalSourceOIDs(*row)
964 switch {
965 case len(sourceOIDs) == 0:
966 errs = append(errs, fmt.Errorf("bgp[%d]: scalar rows without a typed-field source OID require explicit id", rowIdx))
967 case len(sourceOIDs) > 1:
968 errs = append(errs, fmt.Errorf("bgp[%d]: scalar rows with multiple typed-field source OIDs require explicit id", rowIdx))
969 }
970 }
971
972 return errors.Join(errs...)
973 }
974
975 func validateBGPGroupKindCompatibility(rowIdx int, row *BGPConfig) error {
976 var errs []error
977 ForEachBGPSignalValue(*row, func(path string, _ BGPValueConfig) {
978 switch {
979 case row.Kind == BGPRowKindDevice && !strings.HasPrefix(path, "device_counts."):
980 errs = append(errs, fmt.Errorf("bgp[%d].%s: device rows only support device_counts fields", rowIdx, path))
981 case strings.HasPrefix(path, "device_counts.") && row.Kind != BGPRowKindDevice:
982 errs = append(errs, fmt.Errorf("bgp[%d].%s: device_counts fields require kind=device", rowIdx, path))
983 case (strings.HasPrefix(path, "routes.") || strings.HasPrefix(path, "route_limits.")) && row.Kind != BGPRowKindPeerFamily:
984 errs = append(errs, fmt.Errorf("bgp[%d].%s: route fields require kind=peer_family", rowIdx, path))
985 }
986 })
987 return errors.Join(errs...)
988 }
989
990 func validateEnrichBGPState(path string, state *BGPStateConfig, isTable bool) error {
991 var errs []error
992 if !state.BGPValueConfig.IsSet() {
993 if state.Partial {
994 errs = append(errs, fmt.Errorf("%s.partial: partial requires state value source", path))
995 }
996 return errors.Join(errs...)
997 }
998
999 errs = append(errs, validateEnrichBGPValue(path, &state.BGPValueConfig, isTable))
1000 errs = append(errs, validateBGPPeerStateMapping(path, state))
1001 return errors.Join(errs...)
1002 }
1003
1004 func validateEnrichBGPSignalValues(rowIdx int, row *BGPConfig, isTable bool) error {
1005 var errs []error
1006 errs = append(errs, validateEnrichBGPState(fmt.Sprintf("bgp[%d].state", rowIdx), &row.State, isTable))
1007 errs = append(errs, validateEnrichBGPState(fmt.Sprintf("bgp[%d].previous_state", rowIdx), &row.Previous, isTable))
1008 forEachBGPSignalValueConfig(row, func(path string, value *BGPValueConfig) {
1009 if path == "state" || path == "previous_state" {
1010 return
1011 }
1012 errs = append(errs, validateEnrichBGPValue(fmt.Sprintf("bgp[%d].%s", rowIdx, path), value, isTable))
1013 })
1014 return errors.Join(errs...)
1015 }
1016
1017 func validateEnrichBGPAddressFamilyValue(path string, value *BGPAddressFamilyValueConfig, isTable bool) error {
1018 var errs []error
1019 errs = append(errs, validateEnrichBGPValue(path, &value.BGPValueConfig, isTable))
1020 if !value.AllowPrivate {
1021 errs = append(errs, validateBGPAddressFamilyValues(path, value.BGPValueConfig))
1022 }
1023 return errors.Join(errs...)
1024 }
1025
1026 func validateEnrichBGPSubsequentAddressFamilyValue(path string, value *BGPSubsequentAddressFamilyValueConfig, isTable bool) error {
1027 var errs []error
1028 errs = append(errs, validateEnrichBGPValue(path, &value.BGPValueConfig, isTable))
1029 if !value.AllowPrivate {
1030 errs = append(errs, validateBGPSubsequentAddressFamilyValues(path, value.BGPValueConfig))
1031 }
1032 return errors.Join(errs...)
1033 }
1034
1035 func validateEnrichBGPValue(path string, value *BGPValueConfig, isTable bool) error {
1036 var errs []error
1037 if !value.IsSet() {
1038 return nil
1039 }
1040
1041 if !bgpValueHasSource(*value) {
1042 errs = append(errs, fmt.Errorf("%s: must define value, from, symbol.OID, OID, index, index_from_end, or index_transform", path))
1043 }
1044 errs = append(errs, validateMapping(value.Mapping, MetadataSymbol))
1045 errs = append(errs, validateBGPRowIndexSelector(path, value))
1046
1047 if strings.HasPrefix(value.Name, "_") {
1048 errs = append(errs, fmt.Errorf("%s.name: name %q cannot be underscore-prefixed", path, value.Name))
1049 }
1050 if !isTable {
1051 if value.Table != "" {
1052 errs = append(errs, fmt.Errorf("%s.table: scalar BGP values do not support `table` lookups", path))
1053 }
1054 if value.Index != 0 {
1055 errs = append(errs, fmt.Errorf("%s.index: scalar BGP values do not support `index` lookups", path))
1056 }
1057 if value.IndexFromEnd != 0 {
1058 errs = append(errs, fmt.Errorf("%s.index_from_end: scalar BGP values do not support `index_from_end` lookups", path))
1059 }
1060 if len(value.IndexTransform) > 0 {
1061 errs = append(errs, fmt.Errorf("%s.index_transform: scalar BGP values do not support `index_transform`", path))
1062 }
1063 }
1064 if value.Table != "" && bgpValueSymbolForValidation(*value).OID == "" {
1065 errs = append(errs, fmt.Errorf("%s.table: table lookups require symbol.OID, OID, or from", path))
1066 }
1067 if value.LookupSymbol.OID != "" || value.LookupSymbol.Name != "" {
1068 if value.Table == "" {
1069 errs = append(errs, fmt.Errorf("%s.lookup_symbol: lookup_symbol requires table", path))
1070 }
1071 lookupSymbol := SymbolConfig(value.LookupSymbol)
1072 errs = append(errs, validateEnrichBGPSymbol(path+".lookup_symbol", &lookupSymbol))
1073 value.LookupSymbol = SymbolConfigCompat(lookupSymbol)
1074 }
1075 if value.Symbol.OID != "" || value.Symbol.Name != "" {
1076 errs = append(errs, validateEnrichBGPSymbol(path, &value.Symbol))
1077 }
1078
1079 return errors.Join(errs...)
1080 }
1081
1082 func validateBGPRowIndexSelector(path string, value *BGPValueConfig) error {
1083 var selectors []string
1084 if value.Index != 0 {
1085 selectors = append(selectors, "index")
1086 }
1087 if value.IndexFromEnd != 0 {
1088 selectors = append(selectors, "index_from_end")
1089 }
1090 if len(value.IndexTransform) > 0 {
1091 selectors = append(selectors, "index_transform")
1092 }
1093 if len(selectors) <= 1 {
1094 return nil
1095 }
1096 return fmt.Errorf("%s: index, index_from_end, and index_transform are mutually exclusive (set: %s)", path, strings.Join(selectors, ", "))
1097 }
1098
1099 func validateEnrichBGPSymbol(path string, symbol *SymbolConfig) error {
1100 var errs []error
1101
1102 errs = append(errs, validateEnrichSymbol(symbol, MetadataSymbol))
1103 if strings.HasPrefix(symbol.Name, "_") {
1104 errs = append(errs, fmt.Errorf("%s.symbol: name %q cannot be underscore-prefixed", path, symbol.Name))
1105 }
1106 if symbol.ChartMeta != (ChartMeta{}) {
1107 errs = append(errs, fmt.Errorf("%s.symbol: chart_meta cannot be used in BGP rows", path))
1108 }
1109 if symbol.MetricType != "" {
1110 errs = append(errs, fmt.Errorf("%s.symbol: metric_type cannot be used in BGP rows", path))
1111 }
1112 if symbol.Transform != "" {
1113 errs = append(errs, fmt.Errorf("%s.symbol: transform cannot be used in BGP rows", path))
1114 }
1115 if symbol.ScaleFactor != 0 {
1116 errs = append(errs, fmt.Errorf("%s.symbol: scale_factor cannot be used in BGP rows", path))
1117 }
1118 if symbol.ConstantValueOne {
1119 errs = append(errs, fmt.Errorf("%s.symbol: constant_value_one cannot be used in BGP rows", path))
1120 }
1121
1122 return errors.Join(errs...)
1123 }
1124
1125 func validateBGPPeerStateMapping(path string, state *BGPStateConfig) error {
1126 mapping := effectiveBGPValueMapping(state.BGPValueConfig)
1127 if !mapping.HasItems() {
1128 if state.Partial {
1129 return fmt.Errorf("%s.mapping: partial state mapping requires at least one RFC 4271 state", path)
1130 }
1131 return fmt.Errorf("%s.mapping: state mapping must cover all six RFC 4271 states", path)
1132 }
1133
1134 var errs []error
1135 covered := make(map[BGPPeerState]bool)
1136 for raw, mapped := range mapping.Items {
1137 peerState := BGPPeerState(mapped)
1138 if !IsValidBGPPeerState(peerState) {
1139 errs = append(errs, fmt.Errorf("%s.mapping.items[%s]: invalid BGP peer state %q", path, raw, mapped))
1140 continue
1141 }
1142 covered[peerState] = true
1143 }
1144 for i, partial := range state.PartialStates {
1145 if !IsValidBGPPeerState(partial) {
1146 errs = append(errs, fmt.Errorf("%s.partial_states[%d]: invalid BGP peer state %q", path, i, partial))
1147 }
1148 }
1149 if state.Partial {
1150 return errors.Join(errs...)
1151 }
1152 for _, required := range requiredBGPPeerStates {
1153 if !covered[required] {
1154 errs = append(errs, fmt.Errorf("%s.mapping: missing RFC 4271 state %q", path, required))
1155 }
1156 }
1157 return errors.Join(errs...)
1158 }
1159
1160 func validateBGPAddressFamilyValues(path string, value BGPValueConfig) error {
1161 return validateBGPEnumValues(path, value, func(v string) bool {
1162 return IsValidBGPAddressFamily(BGPAddressFamily(v))
1163 }, "address_family")
1164 }
1165
1166 func validateBGPSubsequentAddressFamilyValues(path string, value BGPValueConfig) error {
1167 return validateBGPEnumValues(path, value, func(v string) bool {
1168 return IsValidBGPSubsequentAddressFamily(BGPSubsequentAddressFamily(v))
1169 }, "subsequent_address_family")
1170 }
1171
1172 func validateBGPEnumValues(path string, value BGPValueConfig, valid func(string) bool, label string) error {
1173 var errs []error
1174 if value.Value != "" && !valid(value.Value) {
1175 errs = append(errs, fmt.Errorf("%s.value: invalid BGP %s %q", path, label, value.Value))
1176 }
1177 for raw, mapped := range effectiveBGPValueMapping(value).Items {
1178 if !valid(mapped) {
1179 errs = append(errs, fmt.Errorf("%s.mapping.items[%s]: invalid BGP %s %q", path, raw, label, mapped))
1180 }
1181 }
1182 return errors.Join(errs...)
1183 }
1184
1185 func effectiveBGPValueMapping(value BGPValueConfig) MappingConfig {
1186 if value.Mapping.HasItems() || value.Mapping.Mode != "" {
1187 return value.Mapping
1188 }
1189 return value.Symbol.Mapping
1190 }
1191
1192 type bgpSignalValidationKey struct {
1193 identity string
1194 path string
1195 }
1196
1197 func validateBGPSignalDuplicates(rowIdx int, row *BGPConfig, seen map[bgpSignalValidationKey]string) error {
1198 var errs []error
1199 identity := BGPStructuralIdentity(*row)
1200 ForEachBGPSignalValue(*row, func(path string, _ BGPValueConfig) {
1201 fullPath := fmt.Sprintf("bgp[%d].%s", rowIdx, path)
1202 key := bgpSignalValidationKey{identity: identity, path: path}
1203 if prev, ok := seen[key]; ok {
1204 errs = append(errs, fmt.Errorf("%s: duplicate BGP field for structural identity %q (first seen at %s)", fullPath, identity, prev))
1205 return
1206 }
1207 seen[key] = fullPath
1208 })
1209 return errors.Join(errs...)
1210 }
1211
1212 func validateBGPFromReferences(rowIdx int, row *BGPConfig) error {
1213 if row.Table.OID == "" {
1214 return nil
1215 }
1216
1217 var errs []error
1218 tableOID := TrimBGPOID(row.Table.OID)
1219 for _, ref := range collectBGPValueReferences(*row) {
1220 if ref.value.From == "" {
1221 continue
1222 }
1223 if ref.value.Table != "" {
1224 continue
1225 }
1226 fromOID := TrimBGPOID(ref.value.From)
1227 if !oidHasPrefix(fromOID, tableOID) {
1228 errs = append(errs, fmt.Errorf("bgp[%d].%s.from: OID %q is outside table %q", rowIdx, ref.path, ref.value.From, row.Table.OID))
1229 }
1230 }
1231 return errors.Join(errs...)
1232 }
1233
1234 func validateBGPTableReferences(rows []BGPConfig) error {
1235 tableNameToOID := make(map[string]string)
1236 for i := range rows {
1237 row := &rows[i]
1238 if row.Table.Name != "" && row.Table.OID != "" {
1239 tableNameToOID[row.Table.Name] = TrimBGPOID(row.Table.OID)
1240 }
1241 }
1242
1243 var errs []error
1244 for rowIdx := range rows {
1245 row := &rows[rowIdx]
1246 for _, ref := range collectBGPValueReferences(*row) {
1247 if ref.value.Table == "" {
1248 continue
1249 }
1250 refTableOID, ok := tableNameToOID[ref.value.Table]
1251 if !ok {
1252 continue
1253 }
1254 sourceOID := TrimBGPOID(BGPValueSourceOID(ref.value))
1255 if sourceOID != "" && !oidHasPrefix(sourceOID, refTableOID) {
1256 errs = append(errs, fmt.Errorf("bgp[%d].%s.table: referenced table %q uses OID %q; source OID %q is outside referenced table", rowIdx, ref.path, ref.value.Table, refTableOID, sourceOID))
1257 }
1258 lookupOID := TrimBGPOID(SymbolConfig(ref.value.LookupSymbol).OID)
1259 if lookupOID != "" && !oidHasPrefix(lookupOID, refTableOID) {
1260 errs = append(errs, fmt.Errorf("bgp[%d].%s.lookup_symbol: referenced table %q uses OID %q; lookup OID %q is outside referenced table", rowIdx, ref.path, ref.value.Table, refTableOID, lookupOID))
1261 }
1262 }
1263 }
1264 return errors.Join(errs...)
1265 }
1266
1267 type bgpValueValidationRef struct {
1268 path string
1269 value BGPValueConfig
1270 }
1271
1272 func collectBGPValueReferences(row BGPConfig) []bgpValueValidationRef {
1273 var values []bgpValueValidationRef
1274 add := func(path string, value BGPValueConfig) {
1275 if value.IsSet() {
1276 values = append(values, bgpValueValidationRef{path: path, value: value})
1277 }
1278 }
1279 add("identity.routing_instance", row.Identity.RoutingInstance)
1280 add("identity.neighbor", row.Identity.Neighbor)
1281 add("identity.remote_as", row.Identity.RemoteAS)
1282 add("identity.address_family", row.Identity.AddressFamily.BGPValueConfig)
1283 add("identity.subsequent_address_family", row.Identity.SubsequentAddressFamily.BGPValueConfig)
1284 add("descriptors.local_address", row.Descriptors.LocalAddress)
1285 add("descriptors.local_as", row.Descriptors.LocalAS)
1286 add("descriptors.local_identifier", row.Descriptors.LocalIdentifier)
1287 add("descriptors.peer_identifier", row.Descriptors.PeerIdentifier)
1288 add("descriptors.peer_type", row.Descriptors.PeerType)
1289 add("descriptors.bgp_version", row.Descriptors.BGPVersion)
1290 add("descriptors.description", row.Descriptors.Description)
1291 ForEachBGPSignalValue(row, add)
1292 return values
1293 }
1294
1295 func bgpRowHasSignalConfigs(row BGPConfig) bool {
1296 hasSignals := false
1297 ForEachBGPSignalValue(row, func(_ string, _ BGPValueConfig) {
1298 hasSignals = true
1299 })
1300 return hasSignals
1301 }
1302
1303 func collectBGPSignalSourceOIDs(row BGPConfig) map[string]struct{} {
1304 oids := make(map[string]struct{})
1305 ForEachBGPSignalValue(row, func(_ string, value BGPValueConfig) {
1306 if oid := BGPValueSourceOID(value); oid != "" {
1307 oids[TrimBGPOID(oid)] = struct{}{}
1308 }
1309 })
1310 return oids
1311 }
1312
1313 func bgpValueHasSource(value BGPValueConfig) bool {
1314 return value.Value != "" ||
1315 value.From != "" ||
1316 value.Symbol.OID != "" ||
1317 value.OID != "" ||
1318 value.Index != 0 ||
1319 value.IndexFromEnd != 0 ||
1320 len(value.IndexTransform) > 0
1321 }
1322
1323 func bgpValueSymbolForValidation(value BGPValueConfig) SymbolConfig {
1324 sym := value.Symbol
1325 if sym.OID == "" {
1326 switch {
1327 case value.From != "":
1328 sym.OID = value.From
1329 case value.OID != "":
1330 sym.OID = value.OID
1331 }
1332 }
1333 return sym
1334 }
1335
1336 func validateEnrichTopologySymbol(topologyIdx int, symbol *SymbolConfig, symbolContext SymbolContext) error {
1337 var errs []error
1338
1339 errs = append(errs, validateEnrichSymbol(symbol, symbolContext))
1340 if strings.HasPrefix(symbol.Name, "_") {
1341 errs = append(errs, fmt.Errorf("topology[%d]: symbol name %q cannot be underscore-prefixed", topologyIdx, symbol.Name))
1342 }
1343 if symbol.ChartMeta != (ChartMeta{}) {
1344 errs = append(errs, fmt.Errorf("topology[%d]: chart_meta cannot be used in topology rows", topologyIdx))
1345 }
1346 if symbol.MetricType != "" {
1347 errs = append(errs, fmt.Errorf("topology[%d]: metric_type cannot be used in topology rows", topologyIdx))
1348 }
1349 if symbol.Mapping.HasItems() || symbol.Mapping.Mode != "" {
1350 errs = append(errs, fmt.Errorf("topology[%d]: mapping cannot be used in topology rows", topologyIdx))
1351 }
1352 if symbol.Transform != "" {
1353 errs = append(errs, fmt.Errorf("topology[%d]: transform cannot be used in topology rows", topologyIdx))
1354 }
1355 if symbol.ScaleFactor != 0 {
1356 errs = append(errs, fmt.Errorf("topology[%d]: scale_factor cannot be used in topology rows", topologyIdx))
1357 }
1358 if symbol.Format != "" {
1359 errs = append(errs, fmt.Errorf("topology[%d]: format cannot be used in topology rows", topologyIdx))
1360 }
1361 if symbol.ConstantValueOne {
1362 errs = append(errs, fmt.Errorf("topology[%d]: constant_value_one cannot be used in topology rows", topologyIdx))
1363 }
1364
1365 return errors.Join(errs...)
1366 }
1367
1368 func validateEnrichGlobalMetricTags(metricTags []GlobalMetricTagConfig) error {
1369 var errs []error
1370 for i := range metricTags {
1371 errs = append(errs, validateEnrichMetricTag(&metricTags[i].MetricTagConfig))
1372 errs = append(errs, validateConsumers(fmt.Sprintf("metric_tags[%d].consumers", i), metricTags[i].Consumers))
1373 }
1374 return errors.Join(errs...)
1375 }
1376
1377 func validateConsumers(path string, consumers ConsumerSet) error {
1378 var errs []error
1379 seen := make(map[ProfileConsumer]int)
1380 for i, consumer := range consumers {
1381 switch consumer {
1382 case ConsumerMetrics, ConsumerTopology, ConsumerLicensing, ConsumerBGP:
1383 default:
1384 errs = append(errs, fmt.Errorf("%s[%d]: invalid consumer %q", path, i, consumer))
1385 continue
1386 }
1387 if firstIdx, ok := seen[consumer]; ok {
1388 errs = append(errs, fmt.Errorf("%s[%d]: duplicate consumer %q (first occurrence at index %d)", path, i, consumer, firstIdx))
1389 continue
1390 }
1391 seen[consumer] = i
1392 }
1393 return errors.Join(errs...)
1394 }
1395
1396 func validateEnrichMetricTags(metricTags []MetricTagConfig) error {
1397 var errs []error
1398 for i := range metricTags {
1399 errs = append(errs, validateEnrichMetricTag(&metricTags[i]))
1400 }
1401 return errors.Join(errs...)
1402 }
1403
1404 func validateEnrichSymbol(symbol *SymbolConfig, symbolContext SymbolContext) error {
1405 var errs []error
1406
1407 if symbol.Name == "" {
1408 errs = append(errs, fmt.Errorf("symbol name missing: name=`%s` oid=`%s`", symbol.Name, symbol.OID))
1409 }
1410 if symbol.OID == "" {
1411 if symbolContext == ColumnSymbol && !symbol.ConstantValueOne {
1412 errs = append(errs, fmt.Errorf("symbol oid or send_as_one missing: name=`%s` oid=`%s`", symbol.Name, symbol.OID))
1413 } else if symbolContext != ColumnSymbol {
1414 errs = append(errs, fmt.Errorf("symbol oid missing: name=`%s` oid=`%s`", symbol.Name, symbol.OID))
1415 }
1416 }
1417 if symbol.ExtractValue != "" {
1418 pattern, err := regexp.Compile(symbol.ExtractValue)
1419 if err != nil {
1420 errs = append(errs, fmt.Errorf("cannot compile `extract_value` (%s): %s", symbol.ExtractValue, err.Error()))
1421 } else {
1422 symbol.ExtractValueCompiled = pattern
1423 }
1424 }
1425 if symbol.MatchPattern != "" {
1426 pattern, err := regexp.Compile(symbol.MatchPattern)
1427 if err != nil {
1428 errs = append(errs, fmt.Errorf("cannot compile `match_pattern` (%s): %s", symbol.MatchPattern, err.Error()))
1429 } else {
1430 symbol.MatchPatternCompiled = pattern
1431 }
1432 }
1433 errs = append(errs, validateMapping(symbol.Mapping, symbolContext))
1434 if symbol.Mapping.EffectiveMode() == MappingModeBitmask && symbol.Mapping.HasItems() && symbol.ScaleFactor != 0 {
1435 errs = append(errs, errors.New("`scale_factor` cannot be used with `mapping.mode: bitmask`"))
1436 }
1437 if symbolContext != ColumnSymbol && symbol.ConstantValueOne {
1438 errs = append(errs, errors.New("`constant_value_one` cannot be used outside of tables"))
1439 }
1440 if (symbolContext != ColumnSymbol && symbolContext != ScalarSymbol) && symbol.MetricType != "" {
1441 errs = append(errs, errors.New("`metric_type` cannot be used outside scalar/table metric symbols and metrics root"))
1442 }
1443
1444 return errors.Join(errs...)
1445 }
1446
1447 func validateEnrichMetricTag(metricTag *MetricTagConfig) error {
1448 var errs []error
1449
1450 if (metricTag.Column.OID != "" || metricTag.Column.Name != "") && (metricTag.Symbol.OID != "" || metricTag.Symbol.Name != "") {
1451 errs = append(errs, fmt.Errorf("metric tag symbol and column cannot be both declared: symbol=%v, column=%v", metricTag.Symbol, metricTag.Column))
1452 }
1453
1454 // Move deprecated metricTag.Column to metricTag.Symbol
1455 if metricTag.Column.OID != "" || metricTag.Column.Name != "" {
1456 metricTag.Symbol = SymbolConfigCompat(metricTag.Column)
1457 metricTag.Column = SymbolConfig{}
1458 }
1459
1460 // OID/Name to Symbol harmonization:
1461 // When users declare metric tag like:
1462 // metric_tags:
1463 // - OID: 1.2.3
1464 // symbol: aSymbol
1465 // this will lead to OID stored as MetricTagConfig.OID and name stored as MetricTagConfig.Symbol.Name
1466 // When this happens, we harmonize by moving MetricTagConfig.OID to MetricTagConfig.Symbol.OID.
1467 if metricTag.OID != "" && metricTag.Symbol.OID != "" {
1468 errs = append(errs, fmt.Errorf("metric tag OID and symbol.OID cannot be both declared: OID=%s, symbol.OID=%s", metricTag.OID, metricTag.Symbol.OID))
1469 }
1470 if metricTag.OID != "" && metricTag.Symbol.OID == "" {
1471 metricTag.Symbol.OID = metricTag.OID
1472 metricTag.OID = ""
1473 }
1474 if isRawIndexMetricTag(*metricTag) {
1475 symbol := SymbolConfig(metricTag.Symbol)
1476 if symbol.Name == "" && metricTag.Tag == "" {
1477 errs = append(errs, errors.New("raw index metric tag requires `tag` or `symbol.name`"))
1478 }
1479 if symbol.ExtractValue != "" {
1480 pattern, err := regexp.Compile(symbol.ExtractValue)
1481 if err != nil {
1482 errs = append(errs, fmt.Errorf("cannot compile `extract_value` (%s): %s", symbol.ExtractValue, err.Error()))
1483 } else {
1484 symbol.ExtractValueCompiled = pattern
1485 }
1486 }
1487 if symbol.MatchPattern != "" {
1488 pattern, err := regexp.Compile(symbol.MatchPattern)
1489 if err != nil {
1490 errs = append(errs, fmt.Errorf("cannot compile `match_pattern` (%s): %s", symbol.MatchPattern, err.Error()))
1491 } else {
1492 symbol.MatchPatternCompiled = pattern
1493 }
1494 }
1495 metricTag.Symbol = SymbolConfigCompat(symbol)
1496 } else if metricTag.Symbol.OID != "" || metricTag.Symbol.Name != "" {
1497 symbol := SymbolConfig(metricTag.Symbol)
1498 errs = append(errs, validateEnrichSymbol(&symbol, MetricTagSymbol))
1499 metricTag.Symbol = SymbolConfigCompat(symbol)
1500 }
1501 if metricTag.LookupSymbol.OID != "" || metricTag.LookupSymbol.Name != "" {
1502 symbol := SymbolConfig(metricTag.LookupSymbol)
1503 errs = append(errs, validateEnrichSymbol(&symbol, MetricTagSymbol))
1504 metricTag.LookupSymbol = SymbolConfigCompat(symbol)
1505 if metricTag.Table == "" {
1506 errs = append(errs, errors.New("`lookup_symbol` requires `table`"))
1507 }
1508 }
1509 if metricTag.Match != "" {
1510 pattern, err := regexp.Compile(metricTag.Match)
1511 if err != nil {
1512 errs = append(errs, fmt.Errorf("cannot compile `match` (`%s`): %s", metricTag.Match, err.Error()))
1513 } else {
1514 metricTag.Pattern = pattern
1515 }
1516 if len(metricTag.Tags) == 0 {
1517 errs = append(errs, fmt.Errorf("`tags` mapping must be provided if `match` (`%s`) is defined", metricTag.Match))
1518 }
1519 }
1520 errs = append(errs, validateMapping(metricTag.Mapping, MetricTagSymbol))
1521 if metricTag.Mapping.HasItems() && metricTag.Tag == "" && metricTag.Symbol.Name == "" {
1522 errs = append(errs, fmt.Errorf("`tag` or `symbol.name` must be provided if `mapping` (`%v`) is defined", metricTag.Mapping))
1523 }
1524 for _, transform := range metricTag.IndexTransform {
1525 if transform.DropRight != 0 && transform.End != 0 {
1526 errs = append(errs, fmt.Errorf("transform rule cannot define both end and drop_right. Invalid rule: %#v", transform))
1527 }
1528 if transform.DropRight == 0 && transform.Start > transform.End && transform.End != 0 {
1529 errs = append(errs, fmt.Errorf("transform rule end should be greater than start. Invalid rule: %#v", transform))
1530 }
1531 }
1532
1533 return errors.Join(errs...)
1534 }
1535
1536 func isRawIndexMetricTag(metricTag MetricTagConfig) bool {
1537 if metricTag.Table != "" || metricTag.Symbol.OID != "" {
1538 return false
1539 }
1540
1541 if metricTag.Index != 0 {
1542 return metricTag.Symbol.Format != "" ||
1543 metricTag.Symbol.ExtractValue != "" ||
1544 metricTag.Symbol.MatchPattern != "" ||
1545 metricTag.Mapping.HasItems()
1546 }
1547
1548 return len(metricTag.IndexTransform) > 0 ||
1549 metricTag.Symbol.Format != "" ||
1550 metricTag.Symbol.ExtractValue != "" ||
1551 metricTag.Symbol.MatchPattern != "" ||
1552 metricTag.Mapping.HasItems()
1553 }
1554
1555 func validateMapping(mapping MappingConfig, symbolContext SymbolContext) error {
1556 if !mapping.HasItems() {
1557 if mapping.Mode != "" {
1558 return errors.New("`mapping.mode` requires `mapping.items`")
1559 }
1560 return nil
1561 }
1562
1563 var errs []error
1564
1565 switch mapping.EffectiveMode() {
1566 case MappingModeExact:
1567 case MappingModeBitmask:
1568 if symbolContext != ScalarSymbol && symbolContext != ColumnSymbol {
1569 errs = append(errs, errors.New("`mapping.mode: bitmask` is only supported for scalar/table metric symbols"))
1570 }
1571 for key, value := range mapping.Items {
1572 bit, err := strconv.ParseInt(key, 10, 64)
1573 if err != nil || bit < 0 || (bit != 0 && bit&(bit-1) != 0) {
1574 errs = append(errs, fmt.Errorf("`mapping.mode: bitmask` requires keys to be 0 or a single power-of-two bit, got %q", key))
1575 }
1576 if value == "" {
1577 errs = append(errs, fmt.Errorf("`mapping.mode: bitmask` requires non-empty values, got empty value for key %q", key))
1578 }
1579 }
1580 default:
1581 errs = append(errs, fmt.Errorf("invalid `mapping.mode` %q", mapping.Mode))
1582 }
1583
1584 return errors.Join(errs...)
1585 }
1586
1587 func validateEnrichVirtualMetrics(metrics []MetricsConfig, topology []TopologyConfig, vmetrics []VirtualMetricConfig) error {
1588 var errs []error
1589
1590 metricSources := collectVirtualMetricSourceSpecs(metrics)
1591 topologySources := collectTopologyMetricSourceNames(topology)
1592
1593 seenNames := make(map[string]int)
1594
1595 for i := range vmetrics {
1596 vm := &vmetrics[i]
1597
1598 if vm.Name == "" {
1599 errs = append(errs, fmt.Errorf("virtual_metrics[%d]: missing name", i))
1600 } else {
1601 if firstIdx, ok := seenNames[vm.Name]; ok {
1602 errs = append(errs, fmt.Errorf("virtual_metrics[%d]: duplicate name %q (first occurrence at index %d)", i, vm.Name, firstIdx))
1603 } else {
1604 seenNames[vm.Name] = i
1605 }
1606 if _, ok := metricSources[vm.Name]; ok {
1607 errs = append(errs, fmt.Errorf("virtual_metrics[%d]: name %q conflicts with an existing metric", i, vm.Name))
1608 }
1609 }
1610
1611 for j, label := range vm.GroupBy {
1612 if label == "" {
1613 errs = append(errs, fmt.Errorf("virtual_metrics[%d].group_by[%d]: label cannot be empty", i, j))
1614 }
1615 }
1616
1617 for j, emitTag := range vm.EmitTags {
1618 if emitTag.Tag == "" {
1619 errs = append(errs, fmt.Errorf("virtual_metrics[%d].emit_tags[%d]: missing tag", i, j))
1620 }
1621 if emitTag.From == "" {
1622 errs = append(errs, fmt.Errorf("virtual_metrics[%d].emit_tags[%d]: missing from", i, j))
1623 }
1624 }
1625
1626 grouped := vm.PerRow || len(vm.GroupBy) > 0
1627
1628 switch {
1629 case len(vm.Sources) == 0 && len(vm.Alternatives) == 0:
1630 errs = append(errs, fmt.Errorf("virtual_metrics[%d]: must define sources or alternatives", i))
1631 case len(vm.Alternatives) == 0:
1632 errs = append(errs, validateVirtualMetricSourcesNotTopology(fmt.Sprintf("virtual_metrics[%d].sources", i), vm.Sources, topologySources))
1633 errs = append(errs, validateVirtualMetricSources(fmt.Sprintf("virtual_metrics[%d].sources", i), vm.Sources, metricSources, grouped))
1634 default:
1635 for j, alt := range vm.Alternatives {
1636 if len(alt.Sources) == 0 {
1637 errs = append(errs, fmt.Errorf("virtual_metrics[%d].alternatives[%d]: must define sources", i, j))
1638 continue
1639 }
1640 errs = append(errs, validateVirtualMetricSourcesNotTopology(fmt.Sprintf("virtual_metrics[%d].alternatives[%d].sources", i, j), alt.Sources, topologySources))
1641 errs = append(errs, validateVirtualMetricSources(fmt.Sprintf("virtual_metrics[%d].alternatives[%d].sources", i, j), alt.Sources, metricSources, grouped))
1642 }
1643 }
1644 }
1645
1646 return errors.Join(errs...)
1647 }
1648
1649 func collectTopologyMetricSourceNames(topology []TopologyConfig) map[string]struct{} {
1650 names := make(map[string]struct{})
1651 for _, topo := range topology {
1652 metric := &topo.MetricsConfig
1653 switch {
1654 case metric.IsScalar():
1655 names[metric.Symbol.Name] = struct{}{}
1656 case metric.IsColumn():
1657 for _, sym := range metric.Symbols {
1658 names[sym.Name] = struct{}{}
1659 }
1660 }
1661 }
1662 return names
1663 }
1664
1665 func validateVirtualMetricSourcesNotTopology(path string, sources []VirtualMetricSourceConfig, topologySources map[string]struct{}) error {
1666 var errs []error
1667 for i, src := range sources {
1668 if _, ok := topologySources[src.Metric]; ok {
1669 errs = append(errs, fmt.Errorf("%s[%d]: topology metric source %q cannot be used by virtual_metrics", path, i, src.Metric))
1670 }
1671 }
1672 return errors.Join(errs...)
1673 }
1674
1675 func validateVirtualMetricSources(path string, sources []VirtualMetricSourceConfig, metricSources map[string]map[string]virtualMetricSourceSpec, grouped bool) error {
1676 var errs []error
1677
1678 var groupTable string
1679 for i, src := range sources {
1680 if src.Metric == "" {
1681 errs = append(errs, fmt.Errorf("%s[%d]: missing metric", path, i))
1682 }
1683 if grouped && src.Table == "" {
1684 errs = append(errs, fmt.Errorf("%s[%d]: missing table", path, i))
1685 }
1686
1687 if src.Metric != "" {
1688 tables, ok := metricSources[src.Metric]
1689 switch {
1690 case !ok:
1691 errs = append(errs, fmt.Errorf("%s[%d]: unknown metric source %q", path, i, src.Metric))
1692 case src.Table == "":
1693 if _, ok := tables[""]; !ok {
1694 errs = append(errs, fmt.Errorf("%s[%d]: missing table for non-scalar source %q", path, i, src.Metric))
1695 }
1696 default:
1697 if _, ok := tables[src.Table]; !ok {
1698 errs = append(errs, fmt.Errorf("%s[%d]: unknown metric/table source %q/%q", path, i, src.Metric, src.Table))
1699 }
1700 }
1701 }
1702
1703 if src.Dim != "" && src.Metric != "" {
1704 tables, ok := metricSources[src.Metric]
1705 if !ok {
1706 continue
1707 }
1708
1709 spec, ok := tables[src.Table]
1710 if !ok && src.Table == "" {
1711 spec, ok = tables[""]
1712 }
1713 if !ok {
1714 continue
1715 }
1716
1717 switch spec.dimSupport.mode {
1718 case virtualMetricDimUnsupported:
1719 errs = append(errs, fmt.Errorf("%s[%d]: dim %q requires a MultiValue source metric (%s)", path, i, src.Dim, formatVirtualMetricSourceRef(src)))
1720 case virtualMetricDimKnown:
1721 if !spec.dimSupport.dims[src.Dim] {
1722 errs = append(errs, fmt.Errorf("%s[%d]: dim %q is not available on %s (available: %s)", path, i, src.Dim, formatVirtualMetricSourceRef(src), strings.Join(virtualMetricSourceAvailableDims(spec), ", ")))
1723 }
1724 }
1725 }
1726
1727 if grouped && src.Table != "" {
1728 if groupTable == "" {
1729 groupTable = src.Table
1730 } else if src.Table != groupTable {
1731 errs = append(errs, fmt.Errorf("%s[%d]: grouped virtual metrics require all sources to use the same table (saw %q and %q)", path, i, groupTable, src.Table))
1732 }
1733 }
1734 }
1735
1736 return errors.Join(errs...)
1737 }
1738
1739 func formatVirtualMetricSourceRef(src VirtualMetricSourceConfig) string {
1740 if src.Table == "" {
1741 return fmt.Sprintf("metric %q", src.Metric)
1742 }
1743 return fmt.Sprintf("metric/table %q/%q", src.Metric, src.Table)
1744 }