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 }