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