@cryptotaxi247 / netdata-1 / commits / aebd505c1

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 {