fix(go.d/ddsnmp): walk cross-table columns when referenced table has no metrics (#20583)
Ilya Mashchenko committed
Jun 26, 2025 at 17:22 UTC
aebd505c12fb473ddda564c5e585e5cb1ce0905f
2 files changed
+435
-56
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+63
@@ -15,6 +15,7 @@ import (
15
16
"github.com/netdata/netdata/go/plugins/logger"
17
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
18
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19
)
20
21
func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger) *Collector {
@@ -28,6 +29,7 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
29
30
for _, prof := range profiles {
31
prof := prof
32
+ handleCrossTableTagsWithoutMetrics(prof)
33
coll.profiles[prof.SourceFile] = &profileState{profile: prof}
34
}
35
@@ -226,3 +228,64 @@ var metricMetaReplacer = strings.NewReplacer(
228
"\r", " ",
229
"\x00", "",
230
)
231
+
232
+// handleCrossTableTagsWithoutMetrics ensures tables referenced only by cross-table tags
233
+// are still walked during collection. Without this, if a table like ifXTable is used
234
+// only for cross-table tags (e.g., getting interface names) but has no metrics defined,
235
+// it won't be walked and the tags will be missing. This creates synthetic metric entries
236
+// for such tables using the longest common OID prefix of the referenced columns.
237
+func handleCrossTableTagsWithoutMetrics(prof *ddsnmp.Profile) {
238
+ if prof.Definition == nil {
239
+ return
240
+ }
241
+
242
+ seenTableNames := make(map[string]bool)
243
+
244
+ for _, m := range prof.Definition.Metrics {
245
+ seenTableNames[m.Table.Name] = true
246
+ }
247
+
248
+ tagCrossTableOnlyOIDs := make(map[string][]string)
249
+
250
+ for _, m := range prof.Definition.Metrics {
251
+ if m.IsScalar() {
252
+ continue
253
+ }
254
+ for _, tag := range m.MetricTags {
255
+ oid := tag.Symbol.OID
256
+ if tag.Table == "" || seenTableNames[tag.Table] || oid == "" {
257
+ continue
258
+ }
259
+ tagCrossTableOnlyOIDs[tag.Table] = append(tagCrossTableOnlyOIDs[tag.Table], oid)
260
+ }
261
+ }
262
+
263
+ for tableName, oids := range tagCrossTableOnlyOIDs {
264
+ slices.Sort(oids)
265
+ oids = slices.Compact(oids)
266
+
267
+ prof.Definition.Metrics = append(prof.Definition.Metrics, ddprofiledefinition.MetricsConfig{
268
+ MIB: fmt.Sprintf("synthetic-%s-MIB", tableName),
269
+ Table: ddprofiledefinition.SymbolConfig{
270
+ OID: longestCommonPrefix(oids),
271
+ Name: tableName,
272
+ },
273
+ })
274
+ }
275
+}
276
+
277
+func longestCommonPrefix(oids []string) string {
278
+ if len(oids) == 0 {
279
+ return ""
280
+ }
281
+ prefix := oids[0]
282
+ for i := 1; i < len(oids); i++ {
283
+ for !strings.HasPrefix(oids[i], prefix) {
284
+ prefix = prefix[0 : len(prefix)-1]
285
+ if len(prefix) == 0 {
286
+ return ""
287
+ }
288
+ }
289
+ }
290
+ return strings.TrimSuffix(prefix, ".")
291
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
+372
-56
@@ -1765,62 +1765,6 @@ func TestTableCollector_Collect(t *testing.T) {
1765
},
1766
expectedError: false,
1767
},
1768
- "cross-table tag when referenced table not walked": {
1769
- profile: &ddsnmp.Profile{
1770
- SourceFile: "test-profile.yaml",
1771
- Definition: &ddprofiledefinition.ProfileDefinition{
1772
- Metrics: []ddprofiledefinition.MetricsConfig{
1773
- {
1774
- Table: ddprofiledefinition.SymbolConfig{
1775
- OID: "1.3.6.1.2.1.2.2",
1776
- Name: "ifTable",
1777
- },
1778
- Symbols: []ddprofiledefinition.SymbolConfig{
1779
- {
1780
- OID: "1.3.6.1.2.1.2.2.1.14",
1781
- Name: "ifInErrors",
1782
- },
1783
- },
1784
- MetricTags: []ddprofiledefinition.MetricTagConfig{
1785
- {
1786
- Tag: "interface",
1787
- Symbol: ddprofiledefinition.SymbolConfigCompat{
1788
- OID: "1.3.6.1.2.1.31.1.1.1.1",
1789
- Name: "ifName",
1790
- },
1791
- Table: "ifXTable",
1792
- },
1793
- },
1794
- },
1795
- // Note: ifXTable is NOT in the metrics list
1796
- },
1797
- },
1798
- },
1799
- setupMock: func(m *snmpmock.MockHandler) {
1800
- // Only walk ifTable
1801
- expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.2.2", []gosnmp.SnmpPDU{
1802
- createCounter32PDU("1.3.6.1.2.1.2.2.1.14.1", 10),
1803
- createCounter32PDU("1.3.6.1.2.1.2.2.1.14.2", 20),
1804
- })
1805
- },
1806
- expectedResult: []ddsnmp.Metric{
1807
- {
1808
- Name: "ifInErrors",
1809
- Value: 10,
1810
- Tags: nil, // No cross-table tag because ifXTable wasn't walked
1811
- MetricType: "rate",
1812
- IsTable: true,
1813
- },
1814
- {
1815
- Name: "ifInErrors",
1816
- Value: 20,
1817
- Tags: nil,
1818
- MetricType: "rate",
1819
- IsTable: true,
1820
- },
1821
- },
1822
- expectedError: false,
1823
- },
1768
"cross-table tag with missing row in referenced table": {
1769
profile: &ddsnmp.Profile{
1770
SourceFile: "test-profile.yaml",
@@ -2366,6 +2310,147 @@ func TestTableCollector_Collect(t *testing.T) {
2310
},
2311
expectedError: false,
2312
},
2313
+ "cross-table tag from table without metrics": {
2314
+ profile: &ddsnmp.Profile{
2315
+ SourceFile: "test-profile.yaml",
2316
+ Definition: &ddprofiledefinition.ProfileDefinition{
2317
+ Metrics: []ddprofiledefinition.MetricsConfig{
2318
+ {
2319
+ Table: ddprofiledefinition.SymbolConfig{
2320
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1",
2321
+ Name: "panEntityFRUModuleTable",
2322
+ },
2323
+ Symbols: []ddprofiledefinition.SymbolConfig{
2324
+ {
2325
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1",
2326
+ Name: "panEntryFRUModulePowerUsed",
2327
+ },
2328
+ },
2329
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2330
+ {
2331
+ Tag: "ent_descr",
2332
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2333
+ OID: "1.3.6.1.2.1.47.1.1.1.1.2",
2334
+ Name: "entPhysicalDescr",
2335
+ },
2336
+ Table: "entPhysicalTable",
2337
+ },
2338
+ },
2339
+ },
2340
+ },
2341
+ },
2342
+ },
2343
+ setupMock: func(m *snmpmock.MockHandler) {
2344
+ // Walk panEntityFRUModuleTable
2345
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.25461.1.1.7.1.2.1", []gosnmp.SnmpPDU{
2346
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 100),
2347
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 150),
2348
+ })
2349
+ // Walk entPhysicalDescr column only (not the whole entPhysicalTable)
2350
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.47.1.1.1.1.2", []gosnmp.SnmpPDU{
2351
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.1", "Power Supply 1"),
2352
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.2", "Power Supply 2"),
2353
+ })
2354
+ },
2355
+ expectedResult: []ddsnmp.Metric{
2356
+ {
2357
+ Name: "panEntryFRUModulePowerUsed",
2358
+ Value: 100,
2359
+ Tags: map[string]string{"ent_descr": "Power Supply 1"},
2360
+ MetricType: "gauge",
2361
+ IsTable: true,
2362
+ },
2363
+ {
2364
+ Name: "panEntryFRUModulePowerUsed",
2365
+ Value: 150,
2366
+ Tags: map[string]string{"ent_descr": "Power Supply 2"},
2367
+ MetricType: "gauge",
2368
+ IsTable: true,
2369
+ },
2370
+ },
2371
+ expectedError: false,
2372
+ },
2373
+ "cross-table multiple tags from same table without metrics": {
2374
+ profile: &ddsnmp.Profile{
2375
+ SourceFile: "test-profile.yaml",
2376
+ Definition: &ddprofiledefinition.ProfileDefinition{
2377
+ Metrics: []ddprofiledefinition.MetricsConfig{
2378
+ {
2379
+ Table: ddprofiledefinition.SymbolConfig{
2380
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1",
2381
+ Name: "panEntityFRUModuleTable",
2382
+ },
2383
+ Symbols: []ddprofiledefinition.SymbolConfig{
2384
+ {
2385
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1",
2386
+ Name: "panEntryFRUModulePowerUsed",
2387
+ },
2388
+ },
2389
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
2390
+ {
2391
+ Tag: "ent_descr",
2392
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2393
+ OID: "1.3.6.1.2.1.47.1.1.1.1.2",
2394
+ Name: "entPhysicalDescr",
2395
+ },
2396
+ Table: "entPhysicalTable",
2397
+ },
2398
+ {
2399
+ Tag: "ent_type",
2400
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
2401
+ OID: "1.3.6.1.2.1.47.1.1.1.1.3",
2402
+ Name: "entPhysicalVendorType",
2403
+ },
2404
+ Table: "entPhysicalTable",
2405
+ },
2406
+ },
2407
+ },
2408
+ },
2409
+ },
2410
+ },
2411
+ setupMock: func(m *snmpmock.MockHandler) {
2412
+ // Walk panEntityFRUModuleTable
2413
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.4.1.25461.1.1.7.1.2.1", []gosnmp.SnmpPDU{
2414
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 100),
2415
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 150),
2416
+ })
2417
+
2418
+ // With preprocessing, should walk the common prefix for entPhysicalTable
2419
+ // Common prefix of 1.3.6.1.2.1.47.1.1.1.1.2 and 1.3.6.1.2.1.47.1.1.1.1.3
2420
+ // is 1.3.6.1.2.1.47.1.1.1.1
2421
+ expectSNMPWalk(m, gosnmp.Version2c, "1.3.6.1.2.1.47.1.1.1.1", []gosnmp.SnmpPDU{
2422
+ // entPhysicalDescr column
2423
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.1", "Power Supply 1"),
2424
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.2", "Power Supply 2"),
2425
+ // entPhysicalVendorType column
2426
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.3.1", "PowerSupplyModule"),
2427
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.3.2", "PowerSupplyModule"),
2428
+ })
2429
+ },
2430
+ expectedResult: []ddsnmp.Metric{
2431
+ {
2432
+ Name: "panEntryFRUModulePowerUsed",
2433
+ Value: 100,
2434
+ Tags: map[string]string{
2435
+ "ent_descr": "Power Supply 1",
2436
+ "ent_type": "PowerSupplyModule",
2437
+ },
2438
+ MetricType: "gauge",
2439
+ IsTable: true,
2440
+ },
2441
+ {
2442
+ Name: "panEntryFRUModulePowerUsed",
2443
+ Value: 150,
2444
+ Tags: map[string]string{
2445
+ "ent_descr": "Power Supply 2",
2446
+ "ent_type": "PowerSupplyModule",
2447
+ },
2448
+ MetricType: "gauge",
2449
+ IsTable: true,
2450
+ },
2451
+ },
2452
+ expectedError: false,
2453
+ },
2454
2455
"basic index tag": {
2456
profile: &ddsnmp.Profile{
@@ -3236,6 +3321,7 @@ func TestTableCollector_Collect(t *testing.T) {
3321
3322
tc.setupMock(mockHandler)
3323
3324
+ handleCrossTableTagsWithoutMetrics(tc.profile)
3325
if err := ddsnmp.CompileTransforms(tc.profile); err != nil {
3326
if tc.expectedError && tc.errorContains != "" && strings.Contains(err.Error(), tc.errorContains) {
3327
return // Expected error during compilation
@@ -3768,6 +3854,236 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
3854
collectCount: 2,
3855
sleepBetween: 10 * time.Millisecond,
3856
},
3857
+ "table cache with cross-table-only columns": {
3858
+ profiles: []*ddsnmp.Profile{
3859
+ {
3860
+ SourceFile: "test-profile.yaml",
3861
+ Definition: &ddprofiledefinition.ProfileDefinition{
3862
+ Metrics: []ddprofiledefinition.MetricsConfig{
3863
+ {
3864
+ Table: ddprofiledefinition.SymbolConfig{
3865
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1",
3866
+ Name: "panEntityFRUModuleTable",
3867
+ },
3868
+ Symbols: []ddprofiledefinition.SymbolConfig{
3869
+ {
3870
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1",
3871
+ Name: "panEntryFRUModulePowerUsed",
3872
+ },
3873
+ },
3874
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3875
+ {
3876
+ Tag: "ent_descr",
3877
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3878
+ OID: "1.3.6.1.2.1.47.1.1.1.1.2",
3879
+ Name: "entPhysicalDescr",
3880
+ },
3881
+ Table: "entPhysicalTable",
3882
+ },
3883
+ },
3884
+ },
3885
+ },
3886
+ },
3887
+ },
3888
+ },
3889
+ setupMock: func(m *snmpmock.MockHandler) {
3890
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
3891
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
3892
+
3893
+ // First collection: Walk both table and cross-table column
3894
+
3895
+ // Walk main table
3896
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.25461.1.1.7.1.2.1").Return(
3897
+ []gosnmp.SnmpPDU{
3898
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 100),
3899
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 150),
3900
+ }, nil,
3901
+ ).Times(1)
3902
+
3903
+ // Walk cross-table column
3904
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.47.1.1.1.1.2").Return(
3905
+ []gosnmp.SnmpPDU{
3906
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.1", "Power Supply 1"),
3907
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.2", "Power Supply 2"),
3908
+ }, nil,
3909
+ ).Times(1)
3910
+
3911
+ // Second collection: GET metrics only, cross-table column should be cached
3912
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
3913
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1",
3914
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2",
3915
+ })).Return(
3916
+ &gosnmp.SnmpPacket{
3917
+ Variables: []gosnmp.SnmpPDU{
3918
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 110),
3919
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 160),
3920
+ },
3921
+ }, nil,
3922
+ ).Times(1)
3923
+
3924
+ // Third collection: Still using cache
3925
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
3926
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1",
3927
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2",
3928
+ })).Return(
3929
+ &gosnmp.SnmpPacket{
3930
+ Variables: []gosnmp.SnmpPDU{
3931
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 120),
3932
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 170),
3933
+ },
3934
+ }, nil,
3935
+ ).Times(1)
3936
+
3937
+ },
3938
+ expectedResult: []*ddsnmp.ProfileMetrics{
3939
+ {
3940
+ Source: "test-profile.yaml",
3941
+ DeviceMetadata: nil,
3942
+ Metrics: []ddsnmp.Metric{
3943
+ {
3944
+ Name: "panEntryFRUModulePowerUsed",
3945
+ Value: 120, // Latest value
3946
+ Tags: map[string]string{"ent_descr": "Power Supply 1"},
3947
+ MetricType: "gauge",
3948
+ IsTable: true,
3949
+ },
3950
+ {
3951
+ Name: "panEntryFRUModulePowerUsed",
3952
+ Value: 170,
3953
+ Tags: map[string]string{"ent_descr": "Power Supply 2"},
3954
+ MetricType: "gauge",
3955
+ IsTable: true,
3956
+ },
3957
+ },
3958
+ },
3959
+ },
3960
+ expectedError: false,
3961
+ enableCache: true,
3962
+ cacheTTL: 30 * time.Second,
3963
+ collectCount: 3,
3964
+ sleepBetween: 10 * time.Millisecond,
3965
+ },
3966
+ "table cache with multiple cross-table tags from same table": {
3967
+ profiles: []*ddsnmp.Profile{
3968
+ {
3969
+ SourceFile: "test-profile.yaml",
3970
+ Definition: &ddprofiledefinition.ProfileDefinition{
3971
+ Metrics: []ddprofiledefinition.MetricsConfig{
3972
+ {
3973
+ Table: ddprofiledefinition.SymbolConfig{
3974
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1",
3975
+ Name: "panEntityFRUModuleTable",
3976
+ },
3977
+ Symbols: []ddprofiledefinition.SymbolConfig{
3978
+ {
3979
+ OID: "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1",
3980
+ Name: "panEntryFRUModulePowerUsed",
3981
+ },
3982
+ },
3983
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
3984
+ {
3985
+ Tag: "ent_descr",
3986
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3987
+ OID: "1.3.6.1.2.1.47.1.1.1.1.2",
3988
+ Name: "entPhysicalDescr",
3989
+ },
3990
+ Table: "entPhysicalTable",
3991
+ },
3992
+ {
3993
+ Tag: "ent_type",
3994
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
3995
+ OID: "1.3.6.1.2.1.47.1.1.1.1.3",
3996
+ Name: "entPhysicalVendorType",
3997
+ },
3998
+ Table: "entPhysicalTable",
3999
+ },
4000
+ },
4001
+ },
4002
+ },
4003
+ },
4004
+ },
4005
+ },
4006
+ setupMock: func(m *snmpmock.MockHandler) {
4007
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
4008
+ m.EXPECT().Version().Return(gosnmp.Version2c).AnyTimes()
4009
+
4010
+ // First collection: Walk both tables (order doesn't matter)
4011
+ m.EXPECT().BulkWalkAll("1.3.6.1.4.1.25461.1.1.7.1.2.1").Return(
4012
+ []gosnmp.SnmpPDU{
4013
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 100),
4014
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 150),
4015
+ }, nil,
4016
+ ).Times(1)
4017
+
4018
+ m.EXPECT().BulkWalkAll("1.3.6.1.2.1.47.1.1.1.1").Return(
4019
+ []gosnmp.SnmpPDU{
4020
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.1", "Power Supply 1"),
4021
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.2.2", "Power Supply 2"),
4022
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.3.1", "Type A"),
4023
+ createStringPDU("1.3.6.1.2.1.47.1.1.1.1.3.2", "Type B"),
4024
+ }, nil,
4025
+ ).Times(1)
4026
+
4027
+ // Second collection: GET metrics only
4028
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
4029
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1",
4030
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2",
4031
+ })).Return(
4032
+ &gosnmp.SnmpPacket{
4033
+ Variables: []gosnmp.SnmpPDU{
4034
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 110),
4035
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 160),
4036
+ },
4037
+ }, nil,
4038
+ ).Times(1)
4039
+
4040
+ // Third collection: Still using cache
4041
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
4042
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1",
4043
+ "1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2",
4044
+ })).Return(
4045
+ &gosnmp.SnmpPacket{
4046
+ Variables: []gosnmp.SnmpPDU{
4047
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.1", 120),
4048
+ createGauge32PDU("1.3.6.1.4.1.25461.1.1.7.1.2.1.1.1.2", 170),
4049
+ },
4050
+ }, nil,
4051
+ ).Times(1)
4052
+ },
4053
+ expectedResult: []*ddsnmp.ProfileMetrics{
4054
+ {
4055
+ Source: "test-profile.yaml",
4056
+ DeviceMetadata: nil,
4057
+ Metrics: []ddsnmp.Metric{
4058
+ {
4059
+ Name: "panEntryFRUModulePowerUsed",
4060
+ Value: 120, // Latest value
4061
+ Tags: map[string]string{
4062
+ "ent_descr": "Power Supply 1",
4063
+ "ent_type": "Type A",
4064
+ },
4065
+ MetricType: "gauge",
4066
+ IsTable: true,
4067
+ },
4068
+ {
4069
+ Name: "panEntryFRUModulePowerUsed",
4070
+ Value: 170,
4071
+ Tags: map[string]string{
4072
+ "ent_descr": "Power Supply 2",
4073
+ "ent_type": "Type B",
4074
+ },
4075
+ MetricType: "gauge",
4076
+ IsTable: true,
4077
+ },
4078
+ },
4079
+ },
4080
+ },
4081
+ expectedError: false,
4082
+ enableCache: true,
4083
+ cacheTTL: 30 * time.Second,
4084
+ collectCount: 3,
4085
+ sleepBetween: 10 * time.Millisecond,
4086
+ },
4087
}
4088
4089
for name, tc := range tests {