github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/cmd/ocprometheus/config.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  	"fmt"
     9  	"github.com/aristanetworks/glog"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	gnmiUtils "github.com/aristanetworks/goarista/gnmi"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"golang.org/x/exp/maps"
    17  	"gopkg.in/yaml.v2"
    18  )
    19  
    20  // Config is the representation of ocprometheus's YAML config file.
    21  type Config struct {
    22  	// Per-device labels.
    23  	DeviceLabels map[string]prometheus.Labels
    24  
    25  	// Prefixes to subscribe to.
    26  	Subscriptions []string
    27  
    28  	// Metrics to collect and how to munge them.
    29  	Metrics []*MetricDef
    30  
    31  	// Subscribed paths by their origin
    32  	subsByOrigin map[string][]string
    33  
    34  	//DescSubs  paths used
    35  	DescriptionLabelSubscriptions []string `yaml:"description-label-subscriptions,omitempty"`
    36  }
    37  
    38  // MetricDef is the representation of a metric definiton in the config file.
    39  type MetricDef struct {
    40  	// Path is a regexp to match on the Update's full path.
    41  	// The regexp must be a prefix match.
    42  	// The regexp can define named capture groups to use as labels.
    43  	Path string
    44  
    45  	// Path compiled as a regexp.
    46  	re *regexp.Regexp `deepequal:"ignore"`
    47  
    48  	// Metric name.
    49  	Name string
    50  
    51  	// Metric help string.
    52  	Help string
    53  
    54  	// Label to store string values
    55  	ValueLabel string
    56  
    57  	// Default value to display for string values
    58  	DefaultValue float64
    59  
    60  	// Does the metric store a string value
    61  	stringMetric bool
    62  
    63  	// This map contains the metric descriptors for this metric for each device.
    64  	devDesc map[string]*promDesc
    65  
    66  	// This is the default metric descriptor for devices that don't have explicit descs.
    67  	desc *promDesc
    68  }
    69  
    70  type promDesc struct {
    71  	fqName        string
    72  	help          string
    73  	varLabels     []string
    74  	devPermLabels map[string]string // required labels
    75  }
    76  
    77  // metricValues contains the values used in updating a metric
    78  type metricValues struct {
    79  	desc         *prometheus.Desc
    80  	labels       []string
    81  	defaultValue float64
    82  	stringMetric bool
    83  }
    84  
    85  // Parses the config and creates the descriptors for each path and device.
    86  func parseConfig(cfg []byte) (*Config, error) {
    87  	config := &Config{
    88  		DeviceLabels: make(map[string]prometheus.Labels),
    89  	}
    90  	if err := yaml.Unmarshal(cfg, config); err != nil {
    91  		return nil, fmt.Errorf("Failed to parse config: %v", err)
    92  	}
    93  
    94  	config.subsByOrigin = make(map[string][]string)
    95  	config.addSubscriptions(config.Subscriptions)
    96  	descNodes := config.DescriptionLabelSubscriptions[:0]
    97  	for _, p := range config.DescriptionLabelSubscriptions {
    98  		if !strings.HasSuffix(p, "description") {
    99  			glog.V(2).Infof("skipping %s as it is not a description node", p)
   100  			continue
   101  		}
   102  		descNodes = append(descNodes, p)
   103  	}
   104  	config.DescriptionLabelSubscriptions = descNodes
   105  
   106  	for _, def := range config.Metrics {
   107  		def.re = regexp.MustCompile(def.Path)
   108  		// Extract label names
   109  		reNames := def.re.SubexpNames()[1:]
   110  		labelNames := make([]string, len(reNames))
   111  		for i, n := range reNames {
   112  			labelNames[i] = n
   113  			if n == "" {
   114  				labelNames[i] = "unnamedLabel" + strconv.Itoa(i+1)
   115  			}
   116  		}
   117  		if def.ValueLabel != "" {
   118  			labelNames = append(labelNames, def.ValueLabel)
   119  			def.stringMetric = true
   120  		}
   121  		// Create a default descriptor only if there aren't any per-device labels,
   122  		// or if it's explicitly declared
   123  		if len(config.DeviceLabels) == 0 || len(config.DeviceLabels["*"]) > 0 {
   124  			def.desc = &promDesc{
   125  				fqName:        def.Name,
   126  				help:          def.Help,
   127  				varLabels:     labelNames,
   128  				devPermLabels: config.DeviceLabels["*"],
   129  			}
   130  		}
   131  		// Add per-device descriptors
   132  		def.devDesc = make(map[string]*promDesc)
   133  		for device, labels := range config.DeviceLabels {
   134  			if device == "*" {
   135  				continue
   136  			}
   137  			def.devDesc[device] = &promDesc{
   138  				fqName:        def.Name,
   139  				help:          def.Help,
   140  				varLabels:     labelNames,
   141  				devPermLabels: labels,
   142  			}
   143  		}
   144  	}
   145  
   146  	return config, nil
   147  }
   148  
   149  // Returns a struct containing the descriptor corresponding to the device and path, labels
   150  // extracted from the path, the default value for the metric and if it accepts string values.
   151  // If the device and path doesn't match any metrics, returns nil.
   152  func (c *Config) getMetricValues(s source,
   153  	descriptionLabels map[string]map[string]string) *metricValues {
   154  	for _, def := range c.Metrics {
   155  		if groups := def.re.FindStringSubmatch(s.path); groups != nil {
   156  			if def.ValueLabel != "" {
   157  				groups = append(groups, def.ValueLabel)
   158  			}
   159  			promdescVal, ok := def.devDesc[s.addr]
   160  			if !ok {
   161  				promdescVal = def.desc
   162  			}
   163  
   164  			permLabels := make(map[string]string)
   165  			maps.Copy(permLabels, promdescVal.devPermLabels)
   166  
   167  			closestListParent := findClosestList(s.path)
   168  			if labels, ok := descriptionLabels[closestListParent]; ok {
   169  				maps.Copy(permLabels, labels)
   170  			}
   171  			desc := prometheus.NewDesc(promdescVal.fqName, promdescVal.help, promdescVal.varLabels,
   172  				permLabels)
   173  			return &metricValues{desc: desc, labels: groups[1:], defaultValue: def.DefaultValue,
   174  				stringMetric: def.stringMetric}
   175  		}
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func findClosestList(s string) string {
   182  	vals := gnmiUtils.SplitPath(s)
   183  	for i := len(vals) - 2; i >= 0; i-- {
   184  		// simple heuristic to determine if we have a list node instead of converting string
   185  		//  to gNMI path
   186  		if strings.Contains(vals[i], "[") {
   187  			return "/" + strings.Join(vals[:i+1], "/")
   188  		}
   189  	}
   190  	return ""
   191  }
   192  
   193  // Sends all the descriptors to the channel.
   194  func (c *Config) getAllDescs(ch chan<- *prometheus.Desc) {
   195  	for _, def := range c.Metrics {
   196  		// Default descriptor might not be present
   197  		if def.desc != nil {
   198  			ch <- prometheus.NewDesc(def.desc.fqName, def.desc.help,
   199  				def.desc.varLabels, def.desc.devPermLabels)
   200  		}
   201  
   202  		for _, desc := range def.devDesc {
   203  			ch <- prometheus.NewDesc(desc.fqName, desc.help,
   204  				desc.varLabels, desc.devPermLabels)
   205  		}
   206  	}
   207  }
   208  
   209  func (c *Config) addSubscriptions(subscriptions []string) {
   210  	for _, sub := range subscriptions {
   211  		parts := strings.SplitN(sub, ":", 2)
   212  		if len(parts) == 1 || len(parts[0]) == 0 || parts[0][0] == '/' {
   213  			c.subsByOrigin[""] = append(c.subsByOrigin[""], sub)
   214  		} else {
   215  			origin := parts[0]
   216  			c.subsByOrigin[origin] = append(c.subsByOrigin[origin], parts[1])
   217  		}
   218  	}
   219  }