master
go 129 lines 3.89 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ddsnmp
4
5 import (
6 "fmt"
7 "slices"
8 "strings"
9
10 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
11 )
12
13 // HandleCrossTableTagsWithoutMetrics is the exported entry point for callers outside
14 // this package (notably tests in ddsnmpcollector) that need to prepare a profile in
15 // isolation, without the additional enrichment performed by FinalizeProfiles.
16 func HandleCrossTableTagsWithoutMetrics(prof *Profile) {
17 handleCrossTableTagsWithoutMetrics(prof)
18 }
19
20 // handleCrossTableTagsWithoutMetrics ensures tables referenced only by cross-table tags
21 // are still walked during collection. Without this, if a table like ifXTable is used
22 // only for cross-table tags (e.g., getting interface names) but has no metrics defined,
23 // it won't be walked and the tags will be missing. This creates synthetic metric entries
24 // for such tables using the longest common OID prefix of the referenced columns, including
25 // lookup columns used by value-based joins.
26 func handleCrossTableTagsWithoutMetrics(prof *Profile) {
27 if prof.Definition == nil {
28 return
29 }
30
31 handleCrossTableTagsWithoutMetricsForRows(&prof.Definition.Metrics)
32 handleCrossTableTagsWithoutMetricsForTopologyRows(&prof.Definition.Topology)
33 }
34
35 func handleCrossTableTagsWithoutMetricsForRows(metrics *[]ddprofiledefinition.MetricsConfig) {
36 seenTableNames := make(map[string]bool)
37 for _, m := range *metrics {
38 seenTableNames[m.Table.Name] = true
39 }
40
41 tagCrossTableOnlyOIDs := crossTableOnlyTagOIDs(*metrics, seenTableNames)
42 for tableName, oids := range tagCrossTableOnlyOIDs {
43 *metrics = append(*metrics, syntheticCrossTableMetric(tableName, oids))
44 }
45 }
46
47 func handleCrossTableTagsWithoutMetricsForTopologyRows(topology *[]ddprofiledefinition.TopologyConfig) {
48 seenTableNames := make(map[string]bool)
49 for _, topo := range *topology {
50 seenTableNames[topo.Table.Name] = true
51 }
52
53 for i := range *topology {
54 topo := &(*topology)[i]
55 tagCrossTableOnlyOIDs := crossTableOnlyTagOIDs([]ddprofiledefinition.MetricsConfig{topo.MetricsConfig}, seenTableNames)
56 for tableName, oids := range tagCrossTableOnlyOIDs {
57 *topology = append(*topology, ddprofiledefinition.TopologyConfig{
58 Kind: topo.Kind,
59 MetricsConfig: syntheticCrossTableMetric(tableName, oids),
60 })
61 seenTableNames[tableName] = true
62 }
63 }
64 }
65
66 func crossTableOnlyTagOIDs(metrics []ddprofiledefinition.MetricsConfig, seenTableNames map[string]bool) map[string][]string {
67 tagCrossTableOnlyOIDs := make(map[string][]string)
68 for _, m := range metrics {
69 if m.IsScalar() {
70 continue
71 }
72 for _, tag := range m.MetricTags {
73 if tag.Table == "" || seenTableNames[tag.Table] {
74 continue
75 }
76 if tag.Symbol.OID != "" {
77 tagCrossTableOnlyOIDs[tag.Table] = append(tagCrossTableOnlyOIDs[tag.Table], tag.Symbol.OID)
78 }
79 if tag.LookupSymbol.OID != "" {
80 tagCrossTableOnlyOIDs[tag.Table] = append(tagCrossTableOnlyOIDs[tag.Table], tag.LookupSymbol.OID)
81 }
82 }
83 }
84 return tagCrossTableOnlyOIDs
85 }
86
87 func syntheticCrossTableMetric(tableName string, oids []string) ddprofiledefinition.MetricsConfig {
88 slices.Sort(oids)
89 oids = slices.Compact(oids)
90
91 return ddprofiledefinition.MetricsConfig{
92 MIB: fmt.Sprintf("synthetic-%s-MIB", tableName),
93 Table: ddprofiledefinition.SymbolConfig{
94 OID: longestCommonPrefix(oids),
95 Name: tableName,
96 },
97 }
98 }
99
100 func longestCommonPrefix(oids []string) string {
101 if len(oids) == 0 {
102 return ""
103 }
104
105 prefixParts := splitOIDParts(oids[0])
106 for i := 1; i < len(oids); i++ {
107 parts := splitOIDParts(oids[i])
108 n := min(len(parts), len(prefixParts))
109
110 j := 0
111 for j < n && prefixParts[j] == parts[j] {
112 j++
113 }
114 prefixParts = prefixParts[:j]
115 if len(prefixParts) == 0 {
116 return ""
117 }
118 }
119
120 return strings.Join(prefixParts, ".")
121 }
122
123 func splitOIDParts(oid string) []string {
124 parts := strings.Split(strings.Trim(oid, "."), ".")
125 if len(parts) == 1 && parts[0] == "" {
126 return nil
127 }
128 return parts
129 }