github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/cmd/ocprometheus/collector_test.go (about)

     1  // Copyright (c) 2017 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"regexp"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/aristanetworks/goarista/gnmi"
    17  	"github.com/aristanetworks/goarista/test"
    18  	pb "github.com/openconfig/gnmi/proto/gnmi"
    19  	"github.com/prometheus/client_golang/prometheus"
    20  	"golang.org/x/exp/maps"
    21  )
    22  
    23  func makeMetrics(cfg *Config, expValues map[source]float64, notification *pb.Notification,
    24  	prevMetrics map[source]*labelledMetric,
    25  	descLabels map[string]map[string]string) map[source]*labelledMetric {
    26  
    27  	expMetrics := map[source]*labelledMetric{}
    28  	if prevMetrics != nil {
    29  		expMetrics = prevMetrics
    30  	}
    31  	for src, v := range expValues {
    32  		metric := cfg.getMetricValues(src, descLabels)
    33  		if metric == nil || metric.desc == nil || metric.labels == nil {
    34  			panic("cfg.getMetricValues returned nil")
    35  		}
    36  		// Preserve current value of labels
    37  		labels := metric.labels
    38  		if _, ok := expMetrics[src]; ok && expMetrics[src] != nil {
    39  			labels = expMetrics[src].labels
    40  		}
    41  
    42  		// Handle string updates
    43  		if notification.Update != nil {
    44  			if update, err := findUpdate(notification, src.path); err == nil {
    45  				val, _, ok := parseValue(update)
    46  				if !ok {
    47  					continue
    48  				}
    49  				if metric.stringMetric {
    50  					strVal, ok := val.(string)
    51  					if !ok {
    52  						strVal = fmt.Sprintf("%.0f", val)
    53  					}
    54  					v = metric.defaultValue
    55  					labels[len(labels)-1] = strVal
    56  				}
    57  			}
    58  		}
    59  		expMetrics[src] = &labelledMetric{
    60  			metric: prometheus.MustNewConstMetric(metric.desc, prometheus.GaugeValue, v,
    61  				labels...),
    62  			labels:       labels,
    63  			defaultValue: metric.defaultValue,
    64  			floatVal:     v,
    65  			stringMetric: metric.stringMetric,
    66  		}
    67  	}
    68  	// Handle deletion
    69  	for key := range expMetrics {
    70  		if _, ok := expValues[key]; !ok {
    71  			delete(expMetrics, key)
    72  		}
    73  	}
    74  	return expMetrics
    75  }
    76  
    77  func findUpdate(notif *pb.Notification, path string) (*pb.Update, error) {
    78  	prefix := notif.Prefix
    79  	for _, v := range notif.Update {
    80  		var fullPath string
    81  		if prefix != nil {
    82  			fullPath = gnmi.StrPath(gnmi.JoinPaths(prefix, v.Path))
    83  		} else {
    84  			fullPath = gnmi.StrPath(v.Path)
    85  		}
    86  		if strings.Contains(path, fullPath) || path == fullPath {
    87  			return v, nil
    88  		}
    89  	}
    90  	return nil, fmt.Errorf("Failed to find matching update for path %v", path)
    91  }
    92  
    93  func makeResponse(notif *pb.Notification) *pb.SubscribeResponse {
    94  	return &pb.SubscribeResponse{
    95  		Response: &pb.SubscribeResponse_Update{Update: notif},
    96  	}
    97  }
    98  
    99  func makePath(pathStr string) *pb.Path {
   100  	splitPath := gnmi.SplitPath(pathStr)
   101  	path, err := gnmi.ParseGNMIElements(splitPath)
   102  	if err != nil {
   103  		return &pb.Path{}
   104  	}
   105  	return path
   106  }
   107  
   108  func TestUpdate(t *testing.T) {
   109  	descLabels := make(map[string]map[string]string)
   110  	config := []byte(`
   111  devicelabels:
   112          10.1.1.1:
   113                  lab1: val1
   114                  lab2: val2
   115          '*':
   116                  lab1: val3
   117                  lab2: val4
   118  subscriptions:
   119          - /Sysdb/environment/cooling/status
   120          - /Sysdb/environment/power/status
   121          - /Sysdb/bridging/igmpsnooping/forwarding/forwarding/status
   122          - /Sysdb/l2discovery/lldp/status
   123  metrics:
   124          - name: fanName
   125            path: /Sysdb/environment/cooling/status/fan/name
   126            help: Fan Name
   127            valuelabel: name
   128            defaultvalue: 2.5
   129          - name: intfCounter
   130            path: /Sysdb/(lag|slice/phy/.+)/intfCounterDir/(?P<intf>.+)/intfCounter
   131            help: Per-Interface Bytes/Errors/Discards Counters
   132          - name: fanSpeed
   133            path: /Sysdb/environment/cooling/status/fan/speed/value
   134            help: Fan Speed
   135          - name: igmpSnoopingInf
   136            path: /Sysdb/igmpsnooping/vlanStatus/(?P<vlan>.+)/ethGroup/(?P<mac>.+)/intf/(?P<intf>.+)
   137            help: IGMP snooping status
   138          - name: lldpNeighborInfo
   139            path: /Sysdb/l2discovery/lldp/status/local/(?P<localIndex>.+)/` +
   140  		`portStatus/(?P<intf>.+)/remoteSystem/(?P<remoteSystemIndex>.+)/sysName/value
   141            help: LLDP metric info
   142            valuelabel: neighborName
   143            defaultvalue: 1`)
   144  	cfg, err := parseConfig(config)
   145  	if err != nil {
   146  		t.Fatalf("Unexpected error: %v", err)
   147  	}
   148  	coll := newCollector(cfg, nil)
   149  
   150  	notif := &pb.Notification{
   151  		Prefix: makePath("Sysdb"),
   152  		Update: []*pb.Update{
   153  			{
   154  				Path: makePath("lag/intfCounterDir/Ethernet1/intfCounter"),
   155  				Val: &pb.TypedValue{
   156  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("42")},
   157  				},
   158  			},
   159  			{
   160  				Path: makePath("environment/cooling/status/fan/speed"),
   161  				Val: &pb.TypedValue{
   162  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("{\"value\": 45}")},
   163  				},
   164  			},
   165  			{
   166  				Path: makePath("igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:01/intf/Cpu"),
   167  				Val: &pb.TypedValue{
   168  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("true")},
   169  				},
   170  			},
   171  			{
   172  				Path: makePath("environment/cooling/status/fan/name"),
   173  				Val: &pb.TypedValue{
   174  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("\"Fan1.1\"")},
   175  				},
   176  			},
   177  			{
   178  				Path: makePath("l2discovery/lldp/status/local/1/portStatus/" +
   179  					"Ethernet24/remoteSystem/17/sysName/value"),
   180  				Val: &pb.TypedValue{
   181  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("{\"value\": \"testName\"}")},
   182  				},
   183  			},
   184  		},
   185  	}
   186  	expValues := map[source]float64{
   187  		{
   188  			addr: "10.1.1.1",
   189  			path: "/Sysdb/lag/intfCounterDir/Ethernet1/intfCounter",
   190  		}: 42,
   191  		{
   192  			addr: "10.1.1.1",
   193  			path: "/Sysdb/environment/cooling/status/fan/speed/value",
   194  		}: 45,
   195  		{
   196  			addr: "10.1.1.1",
   197  			path: "/Sysdb/igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:01/intf/Cpu",
   198  		}: 1,
   199  		{
   200  			addr: "10.1.1.1",
   201  			path: "/Sysdb/environment/cooling/status/fan/name",
   202  		}: 2.5,
   203  		{
   204  			addr: "10.1.1.1",
   205  			path: "/Sysdb/l2discovery/lldp/status/local/1/portStatus/Ethernet24/" +
   206  				"remoteSystem/17/sysName/value/value",
   207  		}: 1,
   208  	}
   209  	coll.update("10.1.1.1:6042", makeResponse(notif))
   210  	expMetrics := makeMetrics(cfg, expValues, notif, nil, descLabels)
   211  	if !test.DeepEqual(expMetrics, coll.metrics) {
   212  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   213  	}
   214  
   215  	// Update two values, and one path which is not a metric
   216  	notif = &pb.Notification{
   217  		Prefix: makePath("Sysdb"),
   218  		Update: []*pb.Update{
   219  			{
   220  				Path: makePath("lag/intfCounterDir/Ethernet1/intfCounter"),
   221  				Val: &pb.TypedValue{
   222  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("52")},
   223  				},
   224  			},
   225  			{
   226  				Path: makePath("environment/cooling/status/fan/name"),
   227  				Val: &pb.TypedValue{
   228  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("\"Fan2.1\"")},
   229  				},
   230  			},
   231  			{
   232  				Path: makePath("environment/doesntexist/status"),
   233  				Val: &pb.TypedValue{
   234  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("{\"value\": 45}")},
   235  				},
   236  			},
   237  		},
   238  	}
   239  	src := source{
   240  		addr: "10.1.1.1",
   241  		path: "/Sysdb/lag/intfCounterDir/Ethernet1/intfCounter",
   242  	}
   243  	expValues[src] = 52
   244  
   245  	coll.update("10.1.1.1:6042", makeResponse(notif))
   246  	expMetrics = makeMetrics(cfg, expValues, notif, expMetrics, descLabels)
   247  	if !test.DeepEqual(expMetrics, coll.metrics) {
   248  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   249  	}
   250  
   251  	// Same path, different device
   252  	notif = &pb.Notification{
   253  		Prefix: makePath("Sysdb"),
   254  		Update: []*pb.Update{
   255  			{
   256  				Path: makePath("lag/intfCounterDir/Ethernet1/intfCounter"),
   257  				Val: &pb.TypedValue{
   258  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("42")},
   259  				},
   260  			},
   261  		},
   262  	}
   263  	src.addr = "10.1.1.2"
   264  	expValues[src] = 42
   265  
   266  	coll.update("10.1.1.2:6042", makeResponse(notif))
   267  	expMetrics = makeMetrics(cfg, expValues, notif, expMetrics, descLabels)
   268  	if !test.DeepEqual(expMetrics, coll.metrics) {
   269  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   270  	}
   271  
   272  	// Delete a path
   273  	notif = &pb.Notification{
   274  		Prefix: makePath("Sysdb"),
   275  		Delete: []*pb.Path{makePath("lag/intfCounterDir/Ethernet1/intfCounter")},
   276  	}
   277  	src.addr = "10.1.1.1"
   278  	delete(expValues, src)
   279  
   280  	coll.update("10.1.1.1:6042", makeResponse(notif))
   281  	// Delete a path
   282  	notif = &pb.Notification{
   283  		Prefix: nil,
   284  		Delete: []*pb.Path{makePath("Sysdb/environment/cooling/status/fan/name")},
   285  	}
   286  	src.path = "/Sysdb/environment/cooling/status/fan/name"
   287  	delete(expValues, src)
   288  	coll.update("10.1.1.1:6042", makeResponse(notif))
   289  	expMetrics = makeMetrics(cfg, expValues, notif, expMetrics, descLabels)
   290  	if !test.DeepEqual(expMetrics, coll.metrics) {
   291  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   292  	}
   293  
   294  	// Non-numeric update to path without value label
   295  	notif = &pb.Notification{
   296  		Prefix: makePath("Sysdb"),
   297  		Update: []*pb.Update{
   298  			{
   299  				Path: makePath("lag/intfCounterDir/Ethernet1/intfCounter"),
   300  				Val: &pb.TypedValue{
   301  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("\"test\"")},
   302  				},
   303  			},
   304  		},
   305  	}
   306  
   307  	coll.update("10.1.1.1:6042", makeResponse(notif))
   308  	src.addr = "10.1.1.1"
   309  	src.path = "/Sysdb/lag/intfCounterDir/Ethernet1/intfCounter"
   310  	expValues[src] = 0
   311  	expMetrics = makeMetrics(cfg, expValues, notif, expMetrics, descLabels)
   312  	// Don't make new metrics as it should have no effect
   313  	if !test.DeepEqual(expMetrics, coll.metrics) {
   314  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   315  	}
   316  	notif = &pb.Notification{
   317  		Prefix: nil,
   318  		Update: []*pb.Update{
   319  			{
   320  				Path: makePath("/Sysdb/lag/intfCounterDir/Ethernet1/intfCounter"),
   321  				Val: &pb.TypedValue{
   322  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("62")},
   323  				},
   324  			},
   325  		},
   326  	}
   327  	src = source{
   328  		addr: "10.1.1.1",
   329  		path: "/Sysdb/lag/intfCounterDir/Ethernet1/intfCounter",
   330  	}
   331  	expValues[src] = 62
   332  	coll.update("10.1.1.1:6042", makeResponse(notif))
   333  	expMetrics = makeMetrics(cfg, expValues, notif, expMetrics, descLabels)
   334  	if !test.DeepEqual(expMetrics, coll.metrics) {
   335  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   336  	}
   337  }
   338  
   339  func TestCoalescedDelete(t *testing.T) {
   340  	descLabels := make(map[string]map[string]string)
   341  	config := []byte(`
   342  devicelabels:
   343          10.1.1.1:
   344                  lab1: val1
   345                  lab2: val2
   346          '*':
   347                  lab1: val3
   348                  lab2: val4
   349  subscriptions:
   350          - /Sysdb/environment/cooling/status
   351          - /Sysdb/environment/power/status
   352          - /Sysdb/bridging/igmpsnooping/forwarding/forwarding/status
   353  metrics:
   354          - name: fanName
   355            path: /Sysdb/environment/cooling/status/fan/name
   356            help: Fan Name
   357            valuelabel: name
   358            defaultvalue: 2.5
   359          - name: intfCounter
   360            path: /Sysdb/(lag|slice/phy/.+)/intfCounterDir/(?P<intf>.+)/intfCounter
   361            help: Per-Interface Bytes/Errors/Discards Counters
   362          - name: fanSpeed
   363            path: /Sysdb/environment/cooling/status/fan/speed/value
   364            help: Fan Speed
   365          - name: igmpSnoopingInf
   366            path: /Sysdb/igmpsnooping/vlanStatus/(?P<vlan>.+)/ethGroup/(?P<mac>.+)/intf/(?P<intf>.+)
   367            help: IGMP snooping status`)
   368  	cfg, err := parseConfig(config)
   369  	if err != nil {
   370  		t.Fatalf("Unexpected error: %v", err)
   371  	}
   372  	coll := newCollector(cfg, nil)
   373  
   374  	notif := &pb.Notification{
   375  		Prefix: makePath("Sysdb"),
   376  		Update: []*pb.Update{
   377  			{
   378  				Path: makePath("igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:01/intf/Cpu"),
   379  				Val: &pb.TypedValue{
   380  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("true")},
   381  				},
   382  			},
   383  			{
   384  				Path: makePath("igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:02/intf/Cpu"),
   385  				Val: &pb.TypedValue{
   386  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("true")},
   387  				},
   388  			},
   389  			{
   390  				Path: makePath("igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:03/intf/Cpu"),
   391  				Val: &pb.TypedValue{
   392  					Value: &pb.TypedValue_JsonVal{JsonVal: []byte("true")},
   393  				},
   394  			},
   395  		},
   396  	}
   397  	expValues := map[source]float64{
   398  		{
   399  			addr: "10.1.1.1",
   400  			path: "/Sysdb/igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:01/intf/Cpu",
   401  		}: 1,
   402  		{
   403  			addr: "10.1.1.1",
   404  			path: "/Sysdb/igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:02/intf/Cpu",
   405  		}: 1,
   406  		{
   407  			addr: "10.1.1.1",
   408  			path: "/Sysdb/igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:03/intf/Cpu",
   409  		}: 1,
   410  	}
   411  
   412  	coll.update("10.1.1.1:6042", makeResponse(notif))
   413  	expMetrics := makeMetrics(cfg, expValues, notif, nil, descLabels)
   414  	if !test.DeepEqual(expMetrics, coll.metrics) {
   415  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   416  	}
   417  
   418  	// Delete a subtree
   419  	notif = &pb.Notification{
   420  		Prefix: makePath("Sysdb"),
   421  		Delete: []*pb.Path{makePath("igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:02")},
   422  	}
   423  	src := source{
   424  		addr: "10.1.1.1",
   425  		path: "/Sysdb/igmpsnooping/vlanStatus/2050/ethGroup/01:00:5e:01:01:02/intf/Cpu",
   426  	}
   427  	delete(expValues, src)
   428  
   429  	coll.update("10.1.1.1:6042", makeResponse(notif))
   430  	expMetrics = makeMetrics(cfg, expValues, notif, expMetrics, descLabels)
   431  	if !test.DeepEqual(expMetrics, coll.metrics) {
   432  		t.Errorf("Mismatched metrics: %v", test.Diff(expMetrics, coll.metrics))
   433  	}
   434  
   435  }
   436  
   437  func TestDescriptionTags(t *testing.T) {
   438  	config := []byte(`
   439  devicelabels:
   440          10.1.1.1:
   441                  lab1: val1
   442                  lab2: val2
   443          '*':
   444                  lab1: val3
   445                  lab2: val4
   446  subscriptions:
   447          - /Sysdb/environment/cooling/status
   448          - /Sysdb/environment/power/status
   449          - /Sysdb/bridging/igmpsnooping/forwarding/forwarding/status
   450  metrics:
   451          - name: fanName
   452            path: /Sysdb/environment/cooling/status/fan/name
   453            help: Fan Name
   454            valuelabel: name
   455            defaultvalue: 2.5
   456          - name: intfCounter
   457            path: /Sysdb/(lag|slice/phy/.+)/intfCounterDir/(?P<intf>.+)/intfCounter
   458            help: Per-Interface Bytes/Errors/Discards Counters
   459          - name: fanSpeed
   460            path: /Sysdb/environment/cooling/status/fan/speed/value
   461            help: Fan Speed
   462          - name: igmpSnoopingInf
   463            path: /Sysdb/igmpsnooping/vlanStatus/(?P<vlan>.+)/ethGroup/(?P<mac>.+)/intf/(?P<intf>.+)
   464            help: IGMP snooping status`)
   465  	cfg, err := parseConfig(config)
   466  	if err != nil {
   467  		t.Fatalf("Unexpected error: %v", err)
   468  	}
   469  
   470  	r := regexp.MustCompile(defaultDescriptionRegex)
   471  
   472  	for _, tc := range []struct {
   473  		name     string
   474  		resp     *pb.SubscribeResponse
   475  		expDescs map[string]map[string]string
   476  	}{{
   477  		name:     "no description leafs present",
   478  		expDescs: make(map[string]map[string]string),
   479  	}, {
   480  		name: "add successful interface description node",
   481  		resp: makeResponse(&pb.Notification{
   482  			Update: []*pb.Update{{
   483  				Path: makePath("interfaces/interface[name=Ethernet1]/state/description"),
   484  				Val: &pb.TypedValue{
   485  					Value: &pb.TypedValue_StringVal{StringVal: "[baz][bar=foo]"},
   486  				},
   487  			}},
   488  		}),
   489  		expDescs: map[string]map[string]string{
   490  			"/interfaces/interface[name=Ethernet1]": {
   491  				"baz": "1",
   492  				"bar": "foo",
   493  			},
   494  		},
   495  	}, {
   496  		name: "leaf found with no data",
   497  		resp: makeResponse(&pb.Notification{
   498  			Update: []*pb.Update{{
   499  				Path: makePath("interfaces/interface[name=Ethernet1]/state/description"),
   500  				Val: &pb.TypedValue{
   501  					Value: &pb.TypedValue_StringVal{StringVal: "hello"},
   502  				},
   503  			}},
   504  		}),
   505  		expDescs: make(map[string]map[string]string),
   506  	}, {
   507  		name: "leaf found with invalid regex value group found",
   508  		resp: makeResponse(&pb.Notification{
   509  			Update: []*pb.Update{{
   510  				Path: makePath("interfaces/interface[name=Ethernet1]/state/description"),
   511  				Val: &pb.TypedValue{
   512  					Value: &pb.TypedValue_StringVal{StringVal: "[foo=bar=baz]"},
   513  				},
   514  			}},
   515  		}),
   516  		expDescs: make(map[string]map[string]string),
   517  	}, {
   518  		name: "leaf found with invalid regex key group found",
   519  		resp: makeResponse(&pb.Notification{
   520  			Update: []*pb.Update{{
   521  				Path: makePath("interfaces/interface[name=Ethernet1]/state/description"),
   522  				Val: &pb.TypedValue{
   523  					Value: &pb.TypedValue_StringVal{StringVal: "[fo!oo]"},
   524  				},
   525  			}},
   526  		}),
   527  		expDescs: make(map[string]map[string]string),
   528  	}, {
   529  		name: "add successful peer group description node",
   530  		resp: makeResponse(&pb.Notification{
   531  			Update: []*pb.Update{{
   532  				Path: makePath("/network-instances/network-instance[name=default]/protocols/prot" +
   533  					"ocol[protocol=bgp]/bgp/peer-groups/peer-group[name=foo]/state/description"),
   534  				Val: &pb.TypedValue{
   535  					Value: &pb.TypedValue_StringVal{StringVal: "[baz][bar=foo]"},
   536  				},
   537  			}},
   538  		}),
   539  		expDescs: map[string]map[string]string{
   540  			"/network-instances/network-instance[name=default]/protocols/protocol" +
   541  				"[protocol=bgp]/bgp/peer-groups/peer-group[name=foo]": {
   542  				"baz": "1",
   543  				"bar": "foo",
   544  			},
   545  		},
   546  	}, {
   547  		name: "path with no list key",
   548  		resp: makeResponse(&pb.Notification{
   549  			Update: []*pb.Update{{
   550  				Path: makePath("/system/config/description"),
   551  				Val: &pb.TypedValue{
   552  					Value: &pb.TypedValue_StringVal{StringVal: "[baz][bar=foo]"},
   553  				},
   554  			}},
   555  		}),
   556  		expDescs: make(map[string]map[string]string),
   557  	}} {
   558  		t.Run(tc.name, func(t *testing.T) {
   559  			coll := newCollector(cfg, r)
   560  
   561  			ctx, cancel := context.WithCancel(context.Background())
   562  			defer cancel()
   563  			respCh := make(chan *pb.SubscribeResponse)
   564  			wg := &sync.WaitGroup{}
   565  			wg.Add(1)
   566  
   567  			go coll.handleDescriptionNodes(ctx, respCh, wg)
   568  			if tc.resp != nil {
   569  				respCh <- tc.resp
   570  			}
   571  			respCh <- &pb.SubscribeResponse{
   572  				Response: &pb.SubscribeResponse_SyncResponse{SyncResponse: true},
   573  			}
   574  			wg.Wait()
   575  
   576  			if !test.DeepEqual(tc.expDescs, coll.descriptionLabels) {
   577  				t.Fatalf("unexpected description labels, expected %s, got %s",
   578  					tc.expDescs, coll.descriptionLabels)
   579  			}
   580  		})
   581  	}
   582  
   583  }
   584  
   585  func TestDynamicDescriptionTagUpdate(t *testing.T) {
   586  	config := []byte(`
   587  devicelabels:
   588          10.1.1.1:
   589                  lab1: val1
   590                  lab2: val2
   591          '*':
   592                  lab1: val3
   593                  lab2: val4
   594  subscriptions:
   595          - /interfaces/interface
   596  metrics:
   597          - name: intfCounter
   598            path: /interfaces/interface\[name=(?P<intf>[^\]]+)\]/state/counters/(?P<countertype>.+)
   599  `)
   600  	cfg, err := parseConfig(config)
   601  	if err != nil {
   602  		t.Fatalf("Unexpected error: %v", err)
   603  	}
   604  
   605  	r := regexp.MustCompile(defaultDescriptionRegex)
   606  	ctx, cancel := context.WithCancel(context.Background())
   607  	defer cancel()
   608  	respCh := make(chan *pb.SubscribeResponse)
   609  	wg := &sync.WaitGroup{}
   610  
   611  	// preset inital sync data
   612  	coll := newCollector(cfg, r)
   613  	go coll.handleDescriptionNodes(ctx, respCh, wg)
   614  	wg.Add(1)
   615  	coll.descriptionLabels = map[string]map[string]string{
   616  		"/interfaces/interface[name=Ethernet4]": {"a": "1", "b": "c"},
   617  		"/interfaces/interface[name=Ethernet1]": {"baz": "1", "foo": "bar"},
   618  	}
   619  	respCh <- &pb.SubscribeResponse{
   620  		Response: &pb.SubscribeResponse_SyncResponse{SyncResponse: true},
   621  	}
   622  	wg.Add(1)
   623  
   624  	var expMetrics, prevExpMetrics map[source]*labelledMetric
   625  	for _, tc := range []struct {
   626  		name            string
   627  		notif           *pb.Notification
   628  		descNotif       *pb.Notification
   629  		expValues       map[source]float64
   630  		checkSourceDiff *source
   631  	}{{
   632  		name: "add metric with no description tags present",
   633  		notif: &pb.Notification{
   634  			Update: []*pb.Update{{
   635  				Path: makePath("/interfaces/interface[name=Ethernet999]/state/counters/in-pkts"),
   636  				Val: &pb.TypedValue{
   637  					Value: &pb.TypedValue_IntVal{IntVal: 100},
   638  				}},
   639  			},
   640  		},
   641  		expValues: map[source]float64{{
   642  			addr: "10.1.1.1",
   643  			path: "/interfaces/interface[name=Ethernet999]/state/counters/in-pkts",
   644  		}: 100,
   645  		},
   646  	}, {
   647  		name: "add metric with description tags present",
   648  		notif: &pb.Notification{
   649  			Update: []*pb.Update{{
   650  				Path: makePath("/interfaces/interface[name=Ethernet4]/state/counters/in-pkts"),
   651  				Val: &pb.TypedValue{
   652  					Value: &pb.TypedValue_IntVal{IntVal: 200},
   653  				}},
   654  			},
   655  		},
   656  		expValues: map[source]float64{{
   657  			addr: "10.1.1.1",
   658  			path: "/interfaces/interface[name=Ethernet999]/state/counters/in-pkts",
   659  		}: 100, {
   660  			addr: "10.1.1.1",
   661  			path: "/interfaces/interface[name=Ethernet4]/state/counters/in-pkts",
   662  		}: 200,
   663  		},
   664  	}, {
   665  		name: "add different metric with description tags present",
   666  		notif: &pb.Notification{
   667  			Update: []*pb.Update{{
   668  				Path: makePath("/interfaces/interface[name=Ethernet1]/state/counters/in-pkts"),
   669  				Val: &pb.TypedValue{
   670  					Value: &pb.TypedValue_IntVal{IntVal: 300},
   671  				}},
   672  			},
   673  		},
   674  		expValues: map[source]float64{{
   675  			addr: "10.1.1.1",
   676  			path: "/interfaces/interface[name=Ethernet999]/state/counters/in-pkts",
   677  		}: 100, {
   678  			addr: "10.1.1.1",
   679  			path: "/interfaces/interface[name=Ethernet4]/state/counters/in-pkts",
   680  		}: 200, {
   681  			addr: "10.1.1.1",
   682  			path: "/interfaces/interface[name=Ethernet1]/state/counters/in-pkts",
   683  		}: 300,
   684  		},
   685  	}, {
   686  		name:  "update metric with different tags present",
   687  		notif: &pb.Notification{},
   688  		descNotif: &pb.Notification{
   689  			Update: []*pb.Update{{
   690  				Path: makePath("interfaces/interface[name=Ethernet1]/state/description"),
   691  				Val: &pb.TypedValue{
   692  					Value: &pb.TypedValue_StringVal{StringVal: "[baz]"},
   693  				},
   694  			}},
   695  		},
   696  		expValues: map[source]float64{{
   697  			addr: "10.1.1.1",
   698  			path: "/interfaces/interface[name=Ethernet999]/state/counters/in-pkts",
   699  		}: 100, {
   700  			addr: "10.1.1.1",
   701  			path: "/interfaces/interface[name=Ethernet4]/state/counters/in-pkts",
   702  		}: 200, {
   703  			addr: "10.1.1.1",
   704  			path: "/interfaces/interface[name=Ethernet1]/state/counters/in-pkts",
   705  		}: 300,
   706  		},
   707  		checkSourceDiff: &source{addr: "10.1.1.1",
   708  			path: "/interfaces/interface[name=Ethernet1]/state/counters/in-pkts"},
   709  	}, {
   710  		name:  "update metric with different tags present",
   711  		notif: &pb.Notification{},
   712  		descNotif: &pb.Notification{
   713  			Delete: []*pb.Path{
   714  				makePath("interfaces/interface[name=Ethernet4]/state/description"),
   715  			},
   716  		},
   717  		expValues: map[source]float64{{
   718  			addr: "10.1.1.1",
   719  			path: "/interfaces/interface[name=Ethernet999]/state/counters/in-pkts",
   720  		}: 100, {
   721  			addr: "10.1.1.1",
   722  			path: "/interfaces/interface[name=Ethernet4]/state/counters/in-pkts",
   723  		}: 200, {
   724  			addr: "10.1.1.1",
   725  			path: "/interfaces/interface[name=Ethernet1]/state/counters/in-pkts",
   726  		}: 300,
   727  		},
   728  		checkSourceDiff: &source{addr: "10.1.1.1",
   729  			path: "/interfaces/interface[name=Ethernet4]/state/counters/in-pkts"},
   730  	}} {
   731  		t.Run(tc.name, func(t *testing.T) {
   732  			if tc.descNotif != nil {
   733  				oldDescLabels := make(map[string]map[string]string)
   734  				for k, v := range coll.descriptionLabels {
   735  					innerLabels := make(map[string]string)
   736  					maps.Copy(innerLabels, v)
   737  					oldDescLabels[k] = innerLabels
   738  				}
   739  				respCh <- makeResponse(tc.descNotif)
   740  				// wait for the collector description labels to update
   741  				ticker := time.NewTicker(10 * time.Millisecond)
   742  				loop := true
   743  				for _ = <-ticker.C; loop; {
   744  					for k, v := range oldDescLabels {
   745  						if collV, ok := coll.descriptionLabels[k]; !ok || !maps.Equal(v, collV) {
   746  							loop = false
   747  							break
   748  						}
   749  					}
   750  				}
   751  			}
   752  
   753  			coll.update("10.1.1.1:6042", makeResponse(tc.notif))
   754  			prevExpMetrics = expMetrics
   755  			expMetrics = makeMetrics(cfg, tc.expValues, tc.notif, nil, coll.descriptionLabels)
   756  
   757  			// the permanent labels are private fields, but are shown when stringified
   758  			// so just compare the strings
   759  			if tc.checkSourceDiff != nil {
   760  				currDesc := expMetrics[*tc.checkSourceDiff].metric.Desc().String()
   761  				prefDesc := prevExpMetrics[*tc.checkSourceDiff].metric.Desc().String()
   762  				if currDesc == prefDesc {
   763  					t.Fatalf("expected descriptors to be different, both were %s", currDesc)
   764  				}
   765  			}
   766  
   767  			if !test.DeepEqual(expMetrics, coll.metrics) {
   768  				t.Fatalf("unexpected metrics received, expected %+v, got %+v",
   769  					expMetrics, coll.metrics)
   770  			}
   771  		})
   772  	}
   773  }
   774  
   775  func TestParseValue(t *testing.T) {
   776  	for _, tc := range []struct {
   777  		input     *pb.TypedValue
   778  		expVal    interface{}
   779  		expSuffix string
   780  		expOK     bool
   781  	}{
   782  		{&pb.TypedValue{Value: &pb.TypedValue_JsonVal{JsonVal: []byte("42.42")}},
   783  			float64(42.42),
   784  			"",
   785  			true},
   786  		{&pb.TypedValue{Value: &pb.TypedValue_JsonVal{JsonVal: []byte("-42.42")}},
   787  			float64(-42.42),
   788  			"",
   789  			true},
   790  		{&pb.TypedValue{Value: &pb.TypedValue_DoubleVal{DoubleVal: 42.42}},
   791  			float64(42.42),
   792  			"",
   793  			true},
   794  		{&pb.TypedValue{Value: &pb.TypedValue_DoubleVal{DoubleVal: -42.42}},
   795  			float64(-42.42),
   796  			"",
   797  			true},
   798  		{&pb.TypedValue{Value: &pb.TypedValue_FloatVal{FloatVal: 42.25}},
   799  			float64(42.25),
   800  			"",
   801  			true},
   802  		{&pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: 42}},
   803  			float64(42),
   804  			"",
   805  			true},
   806  	} {
   807  		t.Run("", func(t *testing.T) {
   808  			gotVal, gotSuffix, gotOK := parseValue(&pb.Update{Val: tc.input})
   809  			if gotOK != tc.expOK {
   810  				t.Errorf("expected OK: %t, got: %t", tc.expOK, gotOK)
   811  			}
   812  			if gotSuffix != tc.expSuffix {
   813  				t.Errorf("expected suffix: %q, got: %q", tc.expSuffix, gotSuffix)
   814  			}
   815  			if gotVal != tc.expVal {
   816  				t.Errorf("expected val: %#v, got: %#v", tc.expVal, gotVal)
   817  			}
   818  		})
   819  	}
   820  }