github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/cmd/ocprometheus/main.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  // The ocprometheus implements a Prometheus exporter for OpenConfig telemetry data.
     6  package main
     7  
     8  import (
     9  	"context"
    10  	"flag"
    11  	"fmt"
    12  	"io/ioutil"
    13  	"net/http"
    14  	"regexp"
    15  	"strings"
    16  	"sync"
    17  
    18  	"github.com/aristanetworks/goarista/gnmi"
    19  
    20  	"github.com/aristanetworks/glog"
    21  	pb "github.com/openconfig/gnmi/proto/gnmi"
    22  	"github.com/prometheus/client_golang/prometheus"
    23  	"github.com/prometheus/client_golang/prometheus/promhttp"
    24  	"golang.org/x/sync/errgroup"
    25  )
    26  
    27  // regex to match tags in descriptions e.g. "[foo][bar=baz]"
    28  const defaultDescriptionRegex = `\[([^=\]]+)(=[^]]+)?]`
    29  
    30  func main() {
    31  	// gNMI options
    32  	gNMIcfg := &gnmi.Config{}
    33  	flag.StringVar(&gNMIcfg.Addr, "addr", "localhost", "gNMI gRPC server `address`")
    34  	flag.StringVar(&gNMIcfg.CAFile, "cafile", "", "Path to server TLS certificate file")
    35  	flag.StringVar(&gNMIcfg.CertFile, "certfile", "", "Path to client TLS certificate file")
    36  	flag.StringVar(&gNMIcfg.KeyFile, "keyfile", "", "Path to client TLS private key file")
    37  	flag.StringVar(&gNMIcfg.Username, "username", "", "Username to authenticate with")
    38  	flag.StringVar(&gNMIcfg.Password, "password", "", "Password to authenticate with")
    39  	descRegex := flag.String("description-regex", defaultDescriptionRegex, "custom regex to"+
    40  		" extract labels from description nodes")
    41  	enableDynDescs := flag.Bool("enable-description-labels", false, "disable attaching additional "+
    42  		"labels extracted from description nodes to closest list node children")
    43  	flag.BoolVar(&gNMIcfg.TLS, "tls", false, "Enable TLS")
    44  	flag.StringVar(&gNMIcfg.TLSMinVersion, "tls-min-version", "",
    45  		fmt.Sprintf("Set minimum TLS version for connection (%s)", gnmi.TLSVersions))
    46  	flag.StringVar(&gNMIcfg.TLSMaxVersion, "tls-max-version", "",
    47  		fmt.Sprintf("Set maximum TLS version for connection (%s)", gnmi.TLSVersions))
    48  	subscribePaths := flag.String("subscribe", "/", "Comma-separated list of paths to subscribe to")
    49  
    50  	// program options
    51  	listenaddr := flag.String("listenaddr", ":8080", "Address on which to expose the metrics")
    52  	url := flag.String("url", "/metrics", "URL where to expose the metrics")
    53  	configFlag := flag.String("config", "",
    54  		"Config to turn OpenConfig telemetry into Prometheus metrics")
    55  
    56  	flag.Parse()
    57  	subscriptions := strings.Split(*subscribePaths, ",")
    58  	if *configFlag == "" {
    59  		glog.Fatal("You need specify a config file using -config flag")
    60  	}
    61  	cfg, err := ioutil.ReadFile(*configFlag)
    62  	if err != nil {
    63  		glog.Fatalf("Can't read config file %q: %v", *configFlag, err)
    64  	}
    65  	config, err := parseConfig(cfg)
    66  	if err != nil {
    67  		glog.Fatal(err)
    68  	}
    69  
    70  	// Ignore the default "subscribe-to-everything" subscription of the
    71  	// -subscribe flag.
    72  	if subscriptions[0] == "/" {
    73  		subscriptions = subscriptions[1:]
    74  	}
    75  	// Add to the subscriptions in the config file.
    76  	config.addSubscriptions(subscriptions)
    77  
    78  	var r *regexp.Regexp
    79  	if *enableDynDescs {
    80  		r = regexp.MustCompile(*descRegex)
    81  	}
    82  	coll := newCollector(config, r)
    83  	prometheus.MustRegister(coll)
    84  	ctx := gnmi.NewContext(context.Background(), gNMIcfg)
    85  	client, err := gnmi.Dial(gNMIcfg)
    86  	if err != nil {
    87  		glog.Fatal(err)
    88  	}
    89  
    90  	g, gCtx := errgroup.WithContext(ctx)
    91  	if *enableDynDescs {
    92  		// wait for initial sync to complete before continuing
    93  		wg := &sync.WaitGroup{}
    94  		wg.Add(1)
    95  		go func() {
    96  			if err := subscribeDescriptions(gCtx, client, config.DescriptionLabelSubscriptions,
    97  				coll, wg); err != nil {
    98  				glog.Error(err)
    99  			}
   100  		}()
   101  		wg.Wait()
   102  	}
   103  
   104  	for origin, paths := range config.subsByOrigin {
   105  		subscribeOptions := &gnmi.SubscribeOptions{
   106  			Mode:       "stream",
   107  			StreamMode: "target_defined",
   108  			Paths:      gnmi.SplitPaths(paths),
   109  			Origin:     origin,
   110  		}
   111  		g.Go(func() error {
   112  			return handleSubscription(gCtx, client, subscribeOptions, coll,
   113  				gNMIcfg.Addr)
   114  		})
   115  	}
   116  	http.Handle(*url, promhttp.Handler())
   117  	go http.ListenAndServe(*listenaddr, nil)
   118  	if err := g.Wait(); err != nil {
   119  		glog.Fatal(err)
   120  	}
   121  }
   122  
   123  func handleSubscription(ctx context.Context, client pb.GNMIClient,
   124  	subscribeOptions *gnmi.SubscribeOptions, coll *collector,
   125  	addr string) error {
   126  	respChan := make(chan *pb.SubscribeResponse)
   127  	go func() {
   128  		for resp := range respChan {
   129  			coll.update(addr, resp)
   130  		}
   131  	}()
   132  	return gnmi.SubscribeErr(ctx, client, subscribeOptions, respChan)
   133  }
   134  
   135  // subscribe to the descriptions nodes provided. It will parse the labels out based on the
   136  // default/user defined regex and store it in a map keyed by nearest lsit node.
   137  func subscribeDescriptions(ctx context.Context, client pb.GNMIClient, paths []string,
   138  	coll *collector, wg *sync.WaitGroup) error {
   139  	if len(paths) == 0 {
   140  		glog.V(9).Info("not subscribing to any description nodes as no paths found")
   141  		wg.Done()
   142  		return nil
   143  	}
   144  	var splitPaths [][]string
   145  	for _, p := range paths {
   146  		splitP := strings.Split(strings.TrimPrefix(p, "/"), "/")
   147  		splitPaths = append(splitPaths, splitP)
   148  	}
   149  
   150  	subscribeOptions := &gnmi.SubscribeOptions{
   151  		Mode:       "stream",
   152  		StreamMode: "target_defined",
   153  		Paths:      splitPaths,
   154  	}
   155  	respChan := make(chan *pb.SubscribeResponse)
   156  
   157  	go coll.handleDescriptionNodes(ctx, respChan, wg)
   158  
   159  	return gnmi.SubscribeErr(ctx, client, subscribeOptions, respChan)
   160  }
   161  
   162  // gets the nearest list node from the path, e.g. a/b[foo=bar]/c will return
   163  // a/b[foo=bar]
   164  func getNearestList(p *pb.Path) (*pb.Path, error) {
   165  	elms := p.GetElem()
   166  	var keyLoc int
   167  	for keyLoc = len(elms) - 1; keyLoc != 0; keyLoc-- {
   168  		if len(elms[keyLoc].GetKey()) == 0 {
   169  			continue
   170  		}
   171  		// support can be added for this if needs be, for now skip it for simplicity.
   172  		if len(elms[keyLoc].GetKey()) > 1 {
   173  			return nil, fmt.Errorf("skipping additional labels as it has multiple keys present "+
   174  				"for path %s", p)
   175  		}
   176  		break
   177  	}
   178  	if keyLoc == 0 {
   179  		return nil, fmt.Errorf("unable to find nearest list nodes for path %s", p)
   180  	}
   181  	p.Elem = p.GetElem()[:keyLoc+1]
   182  	return p, nil
   183  }