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 }