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 }