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 }