github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/cmd/ocprometheus/collector.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  	"encoding/json"
     9  	"fmt"
    10  	"math"
    11  	"path"
    12  	"regexp"
    13  	"strings"
    14  	"sync"
    15  
    16  	"golang.org/x/net/context"
    17  
    18  	"github.com/aristanetworks/glog"
    19  	"github.com/aristanetworks/goarista/gnmi"
    20  	gnmiUtils "github.com/aristanetworks/goarista/gnmi"
    21  	pb "github.com/openconfig/gnmi/proto/gnmi"
    22  	"github.com/prometheus/client_golang/prometheus"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/anypb"
    25  )
    26  
    27  var labelRegex = regexp.MustCompile(`[a-zA-Z0-9_-]+`)
    28  
    29  // A metric source.
    30  type source struct {
    31  	addr string
    32  	path string
    33  }
    34  
    35  // Since the labels are fixed per-path and per-device we can cache them here,
    36  // to avoid recomputing them.
    37  type labelledMetric struct {
    38  	metric       prometheus.Metric
    39  	labels       []string
    40  	defaultValue float64
    41  	floatVal     float64
    42  	stringMetric bool
    43  }
    44  
    45  type collector struct {
    46  	// Protects access to metrics map
    47  	m       sync.Mutex
    48  	metrics map[source]*labelledMetric
    49  
    50  	config            *Config
    51  	descRegex         *regexp.Regexp
    52  	descriptionLabels map[string]map[string]string
    53  }
    54  
    55  func newCollector(config *Config, descRegex *regexp.Regexp) *collector {
    56  	return &collector{
    57  		metrics:           make(map[source]*labelledMetric),
    58  		config:            config,
    59  		descriptionLabels: make(map[string]map[string]string),
    60  		descRegex:         descRegex,
    61  	}
    62  }
    63  
    64  // adds the label data to the map from the inital sync. No need to lock the map as we are not
    65  // processing updates yet.
    66  func (c *collector) addInitialDescriptionData(p *pb.Path, val string) {
    67  	labels := extractLabelsFromDesc(val, c.descRegex)
    68  	if len(labels) == 0 {
    69  		return
    70  	}
    71  	c.descriptionLabels[gnmiUtils.StrPath(p)] = labels
    72  }
    73  
    74  // gets updates from the descriptin nodes and updates the map accordingly.
    75  func (c *collector) deleteDescriptionTags(p *pb.Path) {
    76  	c.m.Lock()
    77  	defer c.m.Unlock()
    78  	strP := gnmiUtils.StrPath(p)
    79  	delete(c.descriptionLabels, strP)
    80  	for s, m := range c.metrics {
    81  		if !strings.Contains(s.path, strP) {
    82  			continue
    83  		}
    84  
    85  		metric := c.config.getMetricValues(s, c.descriptionLabels)
    86  		lm := prometheus.MustNewConstMetric(metric.desc, prometheus.GaugeValue, m.floatVal,
    87  			metric.labels...)
    88  		c.metrics[s].metric = lm
    89  	}
    90  }
    91  
    92  func (c *collector) updateDescriptionTags(p *pb.Path, val string) {
    93  	c.m.Lock()
    94  	defer c.m.Unlock()
    95  
    96  	strP := gnmiUtils.StrPath(p)
    97  	labels := extractLabelsFromDesc(val, c.descRegex)
    98  	c.descriptionLabels[strP] = labels
    99  
   100  	for s, m := range c.metrics {
   101  		if !strings.Contains(s.path, strP) {
   102  			continue
   103  		}
   104  
   105  		met := c.config.getMetricValues(s, c.descriptionLabels)
   106  		lm := prometheus.MustNewConstMetric(met.desc, prometheus.GaugeValue, m.floatVal,
   107  			met.labels...)
   108  		c.metrics[s].metric = lm
   109  	}
   110  }
   111  
   112  func (c *collector) handleDescriptionNodes(ctx context.Context,
   113  	respChan chan *pb.SubscribeResponse, wg *sync.WaitGroup) {
   114  	var syncReceived bool
   115  	defer func() {
   116  		if !syncReceived {
   117  			wg.Done()
   118  		}
   119  	}()
   120  
   121  	for {
   122  		select {
   123  		case <-ctx.Done():
   124  			return
   125  		case r := <-respChan:
   126  			// if syncResponse has been received then start subscribing to metric paths
   127  			if r.GetSyncResponse() {
   128  				syncReceived = true
   129  				wg.Done()
   130  				continue
   131  			}
   132  
   133  			notif := r.GetUpdate()
   134  			prefix := notif.GetPrefix()
   135  
   136  			var err error
   137  			if !syncReceived {
   138  				// only updates will be present before syncResponse has been received
   139  				for _, update := range notif.GetUpdate() {
   140  					p := gnmi.JoinPaths(prefix, update.Path)
   141  					p, err = getNearestList(p)
   142  					if err != nil {
   143  						glog.V(9).Infof("failed to parse description tags, got %s", err)
   144  						continue
   145  					}
   146  					c.addInitialDescriptionData(p, update.GetVal().GetStringVal())
   147  				}
   148  				continue
   149  			}
   150  
   151  			// sync received, update data and regen tags if required.
   152  			for _, d := range notif.GetDelete() {
   153  				p := gnmi.JoinPaths(prefix, d)
   154  				p, err = getNearestList(p)
   155  				if err != nil {
   156  					glog.V(9).Infof("failed to parse description tags, got %s", err)
   157  					continue
   158  				}
   159  				c.deleteDescriptionTags(p)
   160  			}
   161  
   162  			for _, u := range notif.GetUpdate() {
   163  				p := gnmi.JoinPaths(prefix, u.Path)
   164  				p, err = getNearestList(p)
   165  				if err != nil {
   166  					glog.V(9).Infof("failed to parse description tags, got %s", err)
   167  					continue
   168  				}
   169  				c.updateDescriptionTags(p, u.GetVal().GetStringVal())
   170  			}
   171  		}
   172  	}
   173  }
   174  
   175  // using the default/user defined regex it extracts the labels from the description node value
   176  func extractLabelsFromDesc(desc string, re *regexp.Regexp) map[string]string {
   177  	labels := make(map[string]string)
   178  	matches := re.FindAllStringSubmatch(desc, -1)
   179  	glog.V(8).Infof("matched the following groups using the provided regex: %v", matches)
   180  
   181  	if len(matches) > 2 {
   182  		glog.V(8).Infof("received more than 2 match groups, got %v", matches)
   183  	}
   184  	for _, match := range matches {
   185  		if match[2] == "" {
   186  			if labelRegex.FindString(match[1]) != match[1] {
   187  				glog.V(9).Infof("label %s did not match allowed regex "+
   188  					"%s", match[1], labelRegex.String())
   189  				continue
   190  			}
   191  			labels[match[1]] = "1"
   192  			glog.V(9).Infof("found label %s=1", match[1])
   193  			continue
   194  		}
   195  
   196  		match[2] = match[2][1:] // remove the equals sign
   197  		if labelRegex.FindString(match[2]) != match[2] {
   198  			glog.V(9).Infof("label %s did not match allowed regex %s",
   199  				match[2], labelRegex.String())
   200  			continue
   201  		}
   202  		labels[match[1]] = match[2]
   203  		glog.V(9).Infof("found label %s%s", match[1], match[2])
   204  	}
   205  	return labels
   206  }
   207  
   208  // Process a notification and update or create the corresponding metrics.
   209  func (c *collector) update(addr string, message proto.Message) {
   210  	resp, ok := message.(*pb.SubscribeResponse)
   211  	if !ok {
   212  		glog.Errorf("Unexpected type of message: %T", message)
   213  		return
   214  	}
   215  
   216  	notif := resp.GetUpdate()
   217  	if notif == nil {
   218  		return
   219  	}
   220  
   221  	device := strings.Split(addr, ":")[0]
   222  	prefix := gnmi.StrPath(notif.Prefix)
   223  	// Process deletes first
   224  	for _, del := range notif.Delete {
   225  		path := path.Join(prefix, gnmi.StrPath(del))
   226  		key := source{addr: device, path: path}
   227  		c.m.Lock()
   228  		if _, ok := c.metrics[key]; ok {
   229  			delete(c.metrics, key)
   230  		} else {
   231  			// TODO: replace this with a prefix tree
   232  			p := path + "/"
   233  			for k := range c.metrics {
   234  				if k.addr == device && strings.HasPrefix(k.path, p) {
   235  					delete(c.metrics, k)
   236  				}
   237  			}
   238  		}
   239  		c.m.Unlock()
   240  	}
   241  
   242  	// Process updates next
   243  	for _, update := range notif.Update {
   244  		path := path.Join(prefix, gnmi.StrPath(update.Path))
   245  		value, suffix, ok := parseValue(update)
   246  		if !ok {
   247  			continue
   248  		}
   249  
   250  		var strUpdate bool
   251  		var floatVal float64
   252  		var strVal string
   253  
   254  		switch v := value.(type) {
   255  		case float64:
   256  			strUpdate = false
   257  			floatVal = v
   258  		case string:
   259  			strUpdate = true
   260  			strVal = v
   261  		}
   262  
   263  		if suffix != "" {
   264  			path += "/" + suffix
   265  		}
   266  
   267  		src := source{addr: device, path: path}
   268  		c.m.Lock()
   269  		// Use the cached labels and descriptor if available
   270  		if m, ok := c.metrics[src]; ok {
   271  			if strUpdate {
   272  				// Skip string updates for non string metrics
   273  				if !m.stringMetric {
   274  					c.m.Unlock()
   275  					continue
   276  				}
   277  				// Display a default value and replace the value label with the string value
   278  				floatVal = m.defaultValue
   279  				m.labels[len(m.labels)-1] = strVal
   280  			}
   281  
   282  			m.metric = prometheus.MustNewConstMetric(m.metric.Desc(), prometheus.GaugeValue,
   283  				floatVal, m.labels...)
   284  			m.floatVal = floatVal
   285  			c.m.Unlock()
   286  			continue
   287  		}
   288  
   289  		// Get the descriptor and labels for this source
   290  		metric := c.config.getMetricValues(src, c.descriptionLabels)
   291  		if metric == nil || metric.desc == nil {
   292  			glog.V(8).Infof("Ignoring unmatched update %v at %s:%s with value %+v",
   293  				update, device, path, value)
   294  			c.m.Unlock()
   295  			continue
   296  		}
   297  
   298  		if metric.stringMetric {
   299  			if !strUpdate {
   300  				// A float was parsed from the update, yet metric expects a string.
   301  				// Store the float as a string.
   302  				strVal = fmt.Sprintf("%.0f", floatVal)
   303  			}
   304  			// Display a default value and replace the value label with the string value
   305  			floatVal = metric.defaultValue
   306  			metric.labels[len(metric.labels)-1] = strVal
   307  		}
   308  
   309  		// Save the metric and labels in the cache
   310  		lm := prometheus.MustNewConstMetric(metric.desc, prometheus.GaugeValue,
   311  			floatVal, metric.labels...)
   312  		c.metrics[src] = &labelledMetric{
   313  			metric:       lm,
   314  			floatVal:     floatVal,
   315  			labels:       metric.labels,
   316  			defaultValue: metric.defaultValue,
   317  			stringMetric: metric.stringMetric,
   318  		}
   319  		c.m.Unlock()
   320  	}
   321  }
   322  
   323  func getValue(intf interface{}) (interface{}, string, bool) {
   324  	switch value := intf.(type) {
   325  	// float64 or string expected as the return value
   326  	case int64:
   327  		return float64(value), "", true
   328  	case uint64:
   329  		return float64(value), "", true
   330  	case float32:
   331  		return float64(value), "", true
   332  	case float64:
   333  		return intf, "", true
   334  	case *pb.Decimal64:
   335  		val := gnmi.DecimalToFloat(value)
   336  		if math.IsInf(val, 0) || math.IsNaN(val) {
   337  			return 0, "", false
   338  		}
   339  		return val, "", true
   340  	case json.Number:
   341  		valFloat, err := value.Float64()
   342  		if err != nil {
   343  			return value, "", true
   344  		}
   345  		return valFloat, "", true
   346  	case *anypb.Any:
   347  		return value.String(), "", true
   348  	case []interface{}:
   349  		glog.V(9).Infof("skipping array value")
   350  	case map[string]interface{}:
   351  		if vIntf, ok := value["value"]; ok {
   352  			res, suffix, ok := getValue(vIntf)
   353  			if suffix != "" {
   354  				return res, fmt.Sprintf("value/%s", suffix), ok
   355  			}
   356  			return res, "value", ok
   357  		}
   358  	case bool:
   359  		if value {
   360  			return float64(1), "", true
   361  		}
   362  		return float64(0), "", true
   363  	case string:
   364  		return value, "", true
   365  	default:
   366  		glog.V(9).Infof("Ignoring update with unexpected type: %T", value)
   367  	}
   368  
   369  	return 0, "", false
   370  }
   371  
   372  // parseValue takes in an update and parses a value and suffix
   373  // Returns an interface that contains either a string or a float64 as well as a suffix
   374  // Unparseable updates return (0, empty string, false)
   375  func parseValue(update *pb.Update) (interface{}, string, bool) {
   376  	intf, err := gnmi.ExtractValue(update)
   377  	if err != nil {
   378  		return 0, "", false
   379  	}
   380  	return getValue(intf)
   381  }
   382  
   383  // Describe implements prometheus.Collector interface
   384  func (c *collector) Describe(ch chan<- *prometheus.Desc) {
   385  	c.config.getAllDescs(ch)
   386  }
   387  
   388  // Collect implements prometheus.Collector interface
   389  func (c *collector) Collect(ch chan<- prometheus.Metric) {
   390  	c.m.Lock()
   391  	for _, m := range c.metrics {
   392  		ch <- m.metric
   393  	}
   394  	c.m.Unlock()
   395  }