master
go 760 lines 20.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ddsnmp
4
5 import (
6 "fmt"
7 "path/filepath"
8 "slices"
9 "sort"
10 "strings"
11
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
13 )
14
15 type scalarMetricKey struct {
16 name string
17 oid string
18 }
19
20 type columnMetricKey struct {
21 table string
22 symbolName string
23 }
24
25 type topologyScalarMetricKey struct {
26 kind ddprofiledefinition.TopologyKind
27 name string
28 oid string
29 }
30
31 type topologyColumnMetricKey struct {
32 kind ddprofiledefinition.TopologyKind
33 table string
34 symbolName string
35 }
36
37 type topologyScalarConflictKey struct {
38 name string
39 oid string
40 }
41
42 type topologyColumnConflictKey struct {
43 table string
44 symbolName string
45 }
46
47 // FindProfiles returns profiles matching the given sysObjectID.
48 // Profiles are sorted by match specificity: most specific first.
49 func FindProfiles(sysObjID, sysDescr string, manualProfiles []string) []*Profile {
50 return DefaultCatalog().Resolve(ResolveRequest{
51 SysObjectID: sysObjID,
52 SysDescr: sysDescr,
53 ManualProfiles: manualProfiles,
54 ManualPolicy: ManualProfileFallback,
55 }).Profiles()
56 }
57
58 // FinalizeProfiles applies load-time profile preparation and deduplicates metrics for a
59 // given profile list. This mirrors the post-processing performed by FindProfiles.
60 func FinalizeProfiles(profiles []*Profile) []*Profile {
61 if len(profiles) == 0 {
62 return nil
63 }
64 for _, prof := range profiles {
65 enrichProfile(prof)
66 handleCrossTableTagsWithoutMetrics(prof)
67 }
68 deduplicateMetricsAcrossProfiles(profiles)
69 return profiles
70 }
71
72 type (
73 Profile struct {
74 SourceFile string `yaml:"-"`
75 Definition *ddprofiledefinition.ProfileDefinition `yaml:",inline"`
76 extensionHierarchy []*extensionInfo
77 }
78 // extensionInfo represents a single extension in the hierarchy
79 extensionInfo struct {
80 name string // Extension name (e.g., "_base.yaml")
81 sourceFile string // Full path to the extension file
82 extensions []*extensionInfo // Nested extensions
83 }
84 )
85
86 // SourceTree returns a string representation of the profile source and its extension hierarchy
87 // Format: "root: [intermediate1: [base], intermediate2]"
88 func (p *Profile) SourceTree() string {
89 rootName := stripFileNameExt(p.SourceFile)
90
91 if len(p.extensionHierarchy) == 0 {
92 return rootName
93 }
94
95 extensions := formatExtensions(p.extensionHierarchy)
96 return fmt.Sprintf("%s: %s", rootName, extensions)
97 }
98
99 // HasExtension returns true if the profile extends the given profile name
100 // (matches either full filename or filename without extension).
101 func (p *Profile) HasExtension(name string) bool {
102 if p == nil {
103 return false
104 }
105 target := stripFileNameExt(name)
106 for _, ext := range p.extensionHierarchy {
107 if extensionHas(ext, target) {
108 return true
109 }
110 }
111 return false
112 }
113
114 func extensionHas(ext *extensionInfo, target string) bool {
115 if ext == nil {
116 return false
117 }
118 if stripFileNameExt(ext.name) == target || stripFileNameExt(ext.sourceFile) == target {
119 return true
120 }
121 for _, child := range ext.extensions {
122 if extensionHas(child, target) {
123 return true
124 }
125 }
126 return false
127 }
128
129 func formatExtensions(extensions []*extensionInfo) string {
130 if len(extensions) == 0 {
131 return "[]"
132 }
133
134 var items []string
135 for _, ext := range extensions {
136 name := stripFileNameExt(ext.sourceFile)
137 if len(ext.extensions) > 0 {
138 items = append(items, fmt.Sprintf("%s: %s", name, formatExtensions(ext.extensions)))
139 } else {
140 items = append(items, name)
141 }
142 }
143
144 return fmt.Sprintf("[%s]", strings.Join(items, ", "))
145 }
146
147 func (p *Profile) clone() *Profile {
148 cloned := &Profile{
149 SourceFile: p.SourceFile,
150 Definition: p.Definition.Clone(),
151 }
152 if p.extensionHierarchy != nil {
153 cloned.extensionHierarchy = cloneExtensionHierarchy(p.extensionHierarchy)
154 }
155 return cloned
156 }
157
158 func cloneExtensionHierarchy(extensions []*extensionInfo) []*extensionInfo {
159 if extensions == nil {
160 return nil
161 }
162
163 cloned := make([]*extensionInfo, len(extensions))
164 for i, ext := range extensions {
165 cloned[i] = &extensionInfo{
166 name: ext.name,
167 sourceFile: ext.sourceFile,
168 extensions: cloneExtensionHierarchy(ext.extensions),
169 }
170 }
171 return cloned
172 }
173
174 func (p *Profile) merge(base *Profile) error {
175 p.mergeMetadata(base)
176 p.mergeMetrics(base)
177 if err := p.mergeTopology(base); err != nil {
178 return err
179 }
180 p.mergeLicensing(base)
181 p.mergeBGP(base)
182 // Append other fields as before (these likely don't need deduplication)
183 p.Definition.MetricTags = append(p.Definition.MetricTags, base.Definition.MetricTags...)
184 p.Definition.StaticTags = append(slices.Clone(base.Definition.StaticTags), p.Definition.StaticTags...)
185 return nil
186 }
187
188 func (p *Profile) mergeMetrics(base *Profile) {
189 seenScalars := make(map[scalarMetricKey]bool)
190 seenColumns := make(map[columnMetricKey]bool)
191 seenTableOIDs := make(map[string]string)
192
193 for _, m := range p.Definition.Metrics {
194 switch {
195 case m.IsScalar():
196 seenScalars[scalarMetricKey{name: m.Symbol.Name, oid: m.Symbol.OID}] = true
197 case m.IsColumn():
198 seenTableOIDs[columnMetricTableIdentity(m.Table)] = m.Table.OID
199 for _, sym := range m.Symbols {
200 seenColumns[columnMetricSymbolKey(m.Table, sym)] = true
201 }
202 }
203 }
204
205 for _, bm := range base.Definition.Metrics {
206 switch {
207 case bm.IsScalar():
208 key := scalarMetricKey{name: bm.Symbol.Name, oid: bm.Symbol.OID}
209 if !seenScalars[key] {
210 p.Definition.Metrics = append(p.Definition.Metrics, bm)
211 seenScalars[key] = true
212 }
213 case bm.IsColumn():
214 tableID := columnMetricTableIdentity(bm.Table)
215 if tableOID, ok := seenTableOIDs[tableID]; ok && tableOID != bm.Table.OID {
216 continue
217 }
218
219 symbols := make([]ddprofiledefinition.SymbolConfig, 0, len(bm.Symbols))
220 for _, sym := range bm.Symbols {
221 key := columnMetricSymbolKey(bm.Table, sym)
222 if seenColumns[key] {
223 continue
224 }
225 symbols = append(symbols, sym)
226 }
227 bm.Symbols = symbols
228 if len(bm.Symbols) > 0 {
229 p.Definition.Metrics = append(p.Definition.Metrics, bm)
230 seenTableOIDs[tableID] = bm.Table.OID
231 }
232 }
233 }
234
235 seenVmetrics := make(map[string]bool)
236
237 for _, m := range p.Definition.VirtualMetrics {
238 seenVmetrics[m.Name] = true
239 }
240 for _, bm := range base.Definition.VirtualMetrics {
241 if !seenVmetrics[bm.Name] {
242 p.Definition.VirtualMetrics = append(p.Definition.VirtualMetrics, bm)
243 seenVmetrics[bm.Name] = true
244 }
245 }
246 }
247
248 func columnMetricSymbolKey(table ddprofiledefinition.SymbolConfig, sym ddprofiledefinition.SymbolConfig) columnMetricKey {
249 return columnMetricKey{
250 table: columnMetricTableIdentity(table),
251 symbolName: sym.Name,
252 }
253 }
254
255 func columnMetricTableIdentity(table ddprofiledefinition.SymbolConfig) string {
256 if table.Name != "" {
257 return table.Name
258 }
259 return table.OID
260 }
261
262 func (p *Profile) mergeTopology(base *Profile) error {
263 seenScalars := make(map[topologyScalarMetricKey]bool)
264 seenColumns := make(map[topologyColumnMetricKey]bool)
265 seenTableOIDs := make(map[string]string)
266 scalarKinds := make(map[topologyScalarConflictKey]ddprofiledefinition.TopologyKind)
267 columnKinds := make(map[topologyColumnConflictKey]ddprofiledefinition.TopologyKind)
268
269 for _, topo := range p.Definition.Topology {
270 if err := indexTopologyMergeConflicts(topo, scalarKinds, columnKinds); err != nil {
271 return err
272 }
273 switch {
274 case topo.IsScalar():
275 seenScalars[topologyScalarMetricKey{kind: topo.Kind, name: topo.Symbol.Name, oid: topo.Symbol.OID}] = true
276 case topo.IsColumn():
277 seenTableOIDs[topologyColumnTableIdentity(topo.Kind, topo.Table)] = topo.Table.OID
278 for _, sym := range topo.Symbols {
279 seenColumns[topologyColumnSymbolKey(topo.Kind, topo.Table, sym)] = true
280 }
281 }
282 }
283
284 for _, baseTopo := range base.Definition.Topology {
285 if err := indexTopologyMergeConflicts(baseTopo, scalarKinds, columnKinds); err != nil {
286 return err
287 }
288 switch {
289 case baseTopo.IsScalar():
290 key := topologyScalarMetricKey{kind: baseTopo.Kind, name: baseTopo.Symbol.Name, oid: baseTopo.Symbol.OID}
291 if !seenScalars[key] {
292 p.Definition.Topology = append(p.Definition.Topology, baseTopo)
293 seenScalars[key] = true
294 }
295 case baseTopo.IsColumn():
296 tableID := topologyColumnTableIdentity(baseTopo.Kind, baseTopo.Table)
297 if tableOID, ok := seenTableOIDs[tableID]; ok && tableOID != baseTopo.Table.OID {
298 continue
299 }
300
301 symbols := make([]ddprofiledefinition.SymbolConfig, 0, len(baseTopo.Symbols))
302 for _, sym := range baseTopo.Symbols {
303 key := topologyColumnSymbolKey(baseTopo.Kind, baseTopo.Table, sym)
304 if seenColumns[key] {
305 continue
306 }
307 symbols = append(symbols, sym)
308 }
309 baseTopo.Symbols = symbols
310 if len(baseTopo.Symbols) > 0 {
311 p.Definition.Topology = append(p.Definition.Topology, baseTopo)
312 seenTableOIDs[tableID] = baseTopo.Table.OID
313 }
314 }
315 }
316
317 return nil
318 }
319
320 func (p *Profile) mergeLicensing(base *Profile) {
321 overridden := make(map[string]bool, len(p.Definition.Licensing))
322 for _, row := range p.Definition.Licensing {
323 overridden[ddprofiledefinition.LicenseMergeIdentity(row)] = true
324 }
325
326 for _, row := range base.Definition.Licensing {
327 if overridden[ddprofiledefinition.LicenseMergeIdentity(row)] {
328 continue
329 }
330 p.Definition.Licensing = append(p.Definition.Licensing, row)
331 }
332 }
333
334 func (p *Profile) mergeBGP(base *Profile) {
335 overridden := make(map[string]bool, len(p.Definition.BGP))
336 for _, row := range p.Definition.BGP {
337 overridden[ddprofiledefinition.BGPMergeIdentity(row)] = true
338 }
339
340 for _, row := range base.Definition.BGP {
341 if overridden[ddprofiledefinition.BGPMergeIdentity(row)] {
342 continue
343 }
344 p.Definition.BGP = append(p.Definition.BGP, row)
345 }
346 }
347
348 func indexTopologyMergeConflicts(
349 topo ddprofiledefinition.TopologyConfig,
350 scalarKinds map[topologyScalarConflictKey]ddprofiledefinition.TopologyKind,
351 columnKinds map[topologyColumnConflictKey]ddprofiledefinition.TopologyKind,
352 ) error {
353 switch {
354 case topo.IsScalar():
355 key := topologyScalarConflictKey{name: topo.Symbol.Name, oid: topo.Symbol.OID}
356 return indexTopologyKindConflict(fmt.Sprintf("scalar %q/%q", topo.Symbol.Name, topo.Symbol.OID), key, topo.Kind, scalarKinds)
357 case topo.IsColumn():
358 for _, sym := range topo.Symbols {
359 key := topologyColumnConflictKey{table: columnMetricTableIdentity(topo.Table), symbolName: sym.Name}
360 if err := indexTopologyKindConflict(fmt.Sprintf("table %q symbol %q", columnMetricTableIdentity(topo.Table), sym.Name), key, topo.Kind, columnKinds); err != nil {
361 return err
362 }
363 }
364 }
365 return nil
366 }
367
368 func indexTopologyKindConflict[K comparable](
369 label string,
370 key K,
371 kind ddprofiledefinition.TopologyKind,
372 seen map[K]ddprofiledefinition.TopologyKind,
373 ) error {
374 if existingKind, ok := seen[key]; ok && existingKind != kind {
375 return fmt.Errorf("conflicting topology kinds for %s: %q and %q", label, existingKind, kind)
376 }
377 seen[key] = kind
378 return nil
379 }
380
381 func topologyColumnSymbolKey(kind ddprofiledefinition.TopologyKind, table ddprofiledefinition.SymbolConfig, sym ddprofiledefinition.SymbolConfig) topologyColumnMetricKey {
382 return topologyColumnMetricKey{
383 kind: kind,
384 table: columnMetricTableIdentity(table),
385 symbolName: sym.Name,
386 }
387 }
388
389 func topologyColumnTableIdentity(kind ddprofiledefinition.TopologyKind, table ddprofiledefinition.SymbolConfig) string {
390 return string(kind) + "|" + columnMetricTableIdentity(table)
391 }
392
393 func (p *Profile) mergeMetadata(base *Profile) {
394 if p.Definition.Metadata == nil {
395 p.Definition.Metadata = make(ddprofiledefinition.MetadataConfig)
396 }
397
398 for resName, baseRes := range base.Definition.Metadata {
399 targetRes, exists := p.Definition.Metadata[resName]
400 if !exists {
401 targetRes = ddprofiledefinition.MetadataResourceConfig{}
402 }
403
404 targetRes.IDTags = append(targetRes.IDTags, baseRes.IDTags...)
405
406 if targetRes.Fields == nil && len(baseRes.Fields) > 0 {
407 targetRes.Fields = make(map[string]ddprofiledefinition.MetadataField, len(baseRes.Fields))
408 }
409
410 for field, symbol := range baseRes.Fields {
411 if _, ok := targetRes.Fields[field]; !ok {
412 targetRes.Fields[field] = symbol
413 }
414 }
415
416 p.Definition.Metadata[resName] = targetRes
417 }
418
419 if len(base.Definition.SysobjectIDMetadata) > 0 {
420 existingOIDs := make(map[string]bool)
421 for _, entry := range p.Definition.SysobjectIDMetadata {
422 existingOIDs[entry.SysobjectID] = true
423 }
424
425 for _, baseEntry := range base.Definition.SysobjectIDMetadata {
426 if !existingOIDs[baseEntry.SysobjectID] {
427 p.Definition.SysobjectIDMetadata = append(p.Definition.SysobjectIDMetadata, baseEntry)
428 }
429 }
430 }
431 }
432
433 func (p *Profile) validate() error {
434 return ddprofiledefinition.ValidateEnrichProfile(p.Definition)
435 }
436
437 func (p *Profile) removeConstantMetrics() {
438 if p.Definition == nil {
439 return
440 }
441
442 p.Definition.Metrics = slices.DeleteFunc(p.Definition.Metrics, func(m ddprofiledefinition.MetricsConfig) bool {
443 if m.IsScalar() && m.Symbol.ConstantValueOne {
444 return true
445 }
446
447 if m.IsColumn() {
448 m.Symbols = slices.DeleteFunc(m.Symbols, func(s ddprofiledefinition.SymbolConfig) bool {
449 return s.ConstantValueOne
450 })
451 }
452
453 return m.IsColumn() && len(m.Symbols) == 0
454 })
455 }
456
457 // sortProfilesBySpecificity sorts profiles by their match specificity.
458 // More specific profiles (longer OIDs, exact matches) come first.
459 // The matchedOIDs map contains the OID that matched for each profile.
460 func sortProfilesBySpecificity(profiles []*Profile, matchedOIDs map[*Profile]string) {
461 slices.SortStableFunc(profiles, func(a, b *Profile) int {
462 aOID := matchedOIDs[a]
463 bOID := matchedOIDs[b]
464
465 // 0) Profiles with an OID match (non-empty) come before descr-only matches (empty)
466 aHasOID := aOID != ""
467 bHasOID := bOID != ""
468 if aHasOID != bHasOID {
469 if aHasOID {
470 return -1
471 }
472 return 1
473 }
474
475 // If both are descr-only (both empty), keep stable order.
476 if !aHasOID && !bHasOID {
477 return 0
478 }
479
480 // 1) Longer OIDs first (more specific)
481 if diff := len(bOID) - len(aOID); diff != 0 {
482 return diff
483 }
484
485 // 2) Same length: exact OIDs before patterns
486 aIsExact := ddprofiledefinition.IsPlainOid(aOID)
487 bIsExact := ddprofiledefinition.IsPlainOid(bOID)
488 if aIsExact != bIsExact {
489 if aIsExact {
490 return -1
491 }
492 return 1
493 }
494
495 // 3) Same type: lexicographic order for stability
496 return strings.Compare(aOID, bOID)
497 })
498 }
499
500 func enrichProfile(prof *Profile) {
501 if prof.Definition == nil {
502 return
503 }
504
505 for i := range prof.Definition.Metrics {
506 enrichMetricTagMappingRefs(prof.Definition.Metrics[i].MetricTags)
507 }
508 for i := range prof.Definition.Topology {
509 enrichMetricTagMappingRefs(prof.Definition.Topology[i].MetricTags)
510 }
511 for i := range prof.Definition.BGP {
512 enrichMetricTagMappingRefs(prof.Definition.BGP[i].MetricTags)
513 }
514 }
515
516 func enrichMetricTagMappingRefs(tags ddprofiledefinition.MetricTagConfigList) {
517 for j := range tags {
518 tagCfg := &tags[j]
519
520 if tagCfg.Mapping.HasItems() {
521 continue
522 }
523
524 switch tagCfg.MappingRef {
525 case "ifType":
526 tagCfg.Mapping = ddprofiledefinition.NewExactMapping(sharedMappings.ifType)
527 case "ifTypeGroup":
528 tagCfg.Mapping = ddprofiledefinition.NewExactMapping(sharedMappings.ifTypeGroup)
529 }
530 }
531 }
532
533 func deduplicateMetricsAcrossProfiles(profiles []*Profile) {
534 if len(profiles) < 2 {
535 return
536 }
537
538 // Profiles are already sorted by specificity from FindProfiles
539 // Just deduplicate metrics, keeping the first occurrence (most specific)
540 seenMetrics := make(map[string]bool)
541 seenVmetrics := make(map[string]bool)
542 seenLicenseSignals := make(map[string]bool)
543 seenBGPSignals := make(map[string]bool)
544
545 for _, prof := range profiles {
546 if prof.Definition == nil {
547 continue
548 }
549
550 prof.Definition.Metrics = slices.DeleteFunc(
551 prof.Definition.Metrics,
552 func(metric ddprofiledefinition.MetricsConfig) bool {
553 key := generateMetricKey(metric)
554 if seenMetrics[key] {
555 return true
556 }
557 seenMetrics[key] = true
558 return false
559 },
560 )
561
562 prof.Definition.VirtualMetrics = slices.DeleteFunc(
563 prof.Definition.VirtualMetrics,
564 func(vm ddprofiledefinition.VirtualMetricConfig) bool {
565 if seenVmetrics[vm.Name] {
566 return true
567 }
568 seenVmetrics[vm.Name] = true
569 return false
570 },
571 )
572
573 deduplicateTopologyInProfile(prof, seenMetrics)
574 deduplicateLicensingInProfile(prof, seenLicenseSignals)
575 deduplicateBGPInProfile(prof, seenBGPSignals)
576 }
577 }
578
579 func deduplicateTopologyInProfile(prof *Profile, seenMetrics map[string]bool) {
580 filtered := prof.Definition.Topology[:0]
581 for _, topo := range prof.Definition.Topology {
582 if topo.IsScalar() {
583 key := generateTopologyScalarMetricKey(topo)
584 if seenMetrics[key] {
585 continue
586 }
587 seenMetrics[key] = true
588 filtered = append(filtered, topo)
589 continue
590 }
591
592 if topo.IsColumn() {
593 symbols := topo.Symbols[:0]
594 for _, sym := range topo.Symbols {
595 key := generateTopologyColumnMetricKey(topo, sym)
596 if seenMetrics[key] {
597 continue
598 }
599 seenMetrics[key] = true
600 symbols = append(symbols, sym)
601 }
602 topo.Symbols = symbols
603 if len(topo.Symbols) == 0 {
604 continue
605 }
606 }
607
608 filtered = append(filtered, topo)
609 }
610 if len(filtered) == 0 {
611 prof.Definition.Topology = nil
612 return
613 }
614 prof.Definition.Topology = filtered
615 }
616
617 func deduplicateLicensingInProfile(prof *Profile, seenSignals map[string]bool) {
618 filtered := prof.Definition.Licensing[:0]
619 for _, row := range prof.Definition.Licensing {
620 keys := generateLicenseSignalKeys(row)
621 if len(keys) == 0 {
622 filtered = append(filtered, row)
623 continue
624 }
625
626 duplicate := false
627 for _, key := range keys {
628 if seenSignals[key] {
629 duplicate = true
630 break
631 }
632 }
633 if duplicate {
634 continue
635 }
636 for _, key := range keys {
637 seenSignals[key] = true
638 }
639 filtered = append(filtered, row)
640 }
641 if len(filtered) == 0 {
642 prof.Definition.Licensing = nil
643 return
644 }
645 prof.Definition.Licensing = filtered
646 }
647
648 func generateLicenseSignalKeys(row ddprofiledefinition.LicensingConfig) []string {
649 identity := ddprofiledefinition.LicenseStructuralIdentity(row)
650 var keys []string
651 add := func(value ddprofiledefinition.LicenseValueConfig) {
652 if value.IsSet() && value.Kind != "" {
653 keys = append(keys, strings.Join([]string{identity, string(value.Kind)}, "|"))
654 }
655 }
656 add(row.State.LicenseValueConfig)
657 addLicenseTimerSignalKeys(row.Signals.Expiry, add)
658 addLicenseTimerSignalKeys(row.Signals.Authorization, add)
659 addLicenseTimerSignalKeys(row.Signals.Certificate, add)
660 addLicenseTimerSignalKeys(row.Signals.Grace, add)
661 add(row.Signals.Usage.Used)
662 add(row.Signals.Usage.Capacity)
663 add(row.Signals.Usage.Available)
664 add(row.Signals.Usage.Percent)
665 return keys
666 }
667
668 func deduplicateBGPInProfile(prof *Profile, seenSignals map[string]bool) {
669 filtered := prof.Definition.BGP[:0]
670 for _, row := range prof.Definition.BGP {
671 keys := generateBGPSignalKeys(row)
672 if len(keys) == 0 {
673 filtered = append(filtered, row)
674 continue
675 }
676
677 duplicate := false
678 for _, key := range keys {
679 if seenSignals[key] {
680 duplicate = true
681 break
682 }
683 }
684 if duplicate {
685 continue
686 }
687 for _, key := range keys {
688 seenSignals[key] = true
689 }
690 filtered = append(filtered, row)
691 }
692 if len(filtered) == 0 {
693 prof.Definition.BGP = nil
694 return
695 }
696 prof.Definition.BGP = filtered
697 }
698
699 func generateBGPSignalKeys(row ddprofiledefinition.BGPConfig) []string {
700 identity := ddprofiledefinition.BGPStructuralIdentity(row)
701 var keys []string
702 ddprofiledefinition.ForEachBGPSignalValue(row, func(path string, _ ddprofiledefinition.BGPValueConfig) {
703 keys = append(keys, strings.Join([]string{identity, path}, "|"))
704 })
705 return keys
706 }
707
708 func addLicenseTimerSignalKeys(cfg ddprofiledefinition.LicenseTimerSignalsConfig, add func(ddprofiledefinition.LicenseValueConfig)) {
709 add(cfg.LicenseValueConfig)
710 add(cfg.Timestamp)
711 add(cfg.Remaining)
712 }
713
714 func generateTopologyScalarMetricKey(topo ddprofiledefinition.TopologyConfig) string {
715 return strings.Join([]string{
716 "topology-scalar",
717 string(topo.Kind),
718 topo.Symbol.OID,
719 topo.Symbol.Name,
720 }, "|")
721 }
722
723 func generateTopologyColumnMetricKey(topo ddprofiledefinition.TopologyConfig, sym ddprofiledefinition.SymbolConfig) string {
724 return strings.Join([]string{
725 "topology-table",
726 string(topo.Kind),
727 topo.Table.OID,
728 columnMetricTableIdentity(topo.Table),
729 sym.Name,
730 }, "|")
731 }
732
733 func generateMetricKey(metric ddprofiledefinition.MetricsConfig) string {
734 var parts []string
735
736 if metric.IsScalar() {
737 parts = append(parts, "scalar")
738 parts = append(parts, metric.Symbol.OID)
739 parts = append(parts, metric.Symbol.Name)
740 return strings.Join(parts, "|")
741 }
742
743 parts = append(parts, "table")
744 parts = append(parts, metric.Table.OID)
745 parts = append(parts, metric.Table.Name)
746
747 symbolKeys := make([]string, 0, len(metric.Symbols))
748 for _, sym := range metric.Symbols {
749 symbolKey := fmt.Sprintf("%s:%s", sym.OID, sym.Name)
750 symbolKeys = append(symbolKeys, symbolKey)
751 }
752 sort.Strings(symbolKeys)
753 parts = append(parts, symbolKeys...)
754
755 return strings.Join(parts, "|")
756 }
757
758 func stripFileNameExt(path string) string {
759 return strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
760 }