2018-01-29 19:44:18 +00:00
|
|
|
// Copyright (c) 2016 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 octsdb tool pushes OpenConfig telemetry to OpenTSDB.
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2018-09-05 15:36:14 +00:00
|
|
|
"context"
|
2018-01-29 19:44:18 +00:00
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2018-09-05 15:36:14 +00:00
|
|
|
"github.com/aristanetworks/goarista/gnmi"
|
2018-01-29 19:44:18 +00:00
|
|
|
|
|
|
|
"github.com/aristanetworks/glog"
|
2018-09-05 15:36:14 +00:00
|
|
|
pb "github.com/openconfig/gnmi/proto/gnmi"
|
2018-01-29 19:44:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
2018-09-05 15:36:14 +00:00
|
|
|
|
|
|
|
// gNMI options
|
|
|
|
cfg := &gnmi.Config{}
|
|
|
|
flag.StringVar(&cfg.Addr, "addr", "localhost", "gNMI gRPC server `address`")
|
|
|
|
flag.StringVar(&cfg.CAFile, "cafile", "", "Path to server TLS certificate file")
|
|
|
|
flag.StringVar(&cfg.CertFile, "certfile", "", "Path to client TLS certificate file")
|
|
|
|
flag.StringVar(&cfg.KeyFile, "keyfile", "", "Path to client TLS private key file")
|
|
|
|
flag.StringVar(&cfg.Username, "username", "", "Username to authenticate with")
|
|
|
|
flag.StringVar(&cfg.Password, "password", "", "Password to authenticate with")
|
|
|
|
flag.BoolVar(&cfg.TLS, "tls", false, "Enable TLS")
|
|
|
|
|
|
|
|
// Program options
|
|
|
|
subscribePaths := flag.String("paths", "/", "Comma-separated list of paths to subscribe to")
|
|
|
|
|
2018-01-29 19:44:18 +00:00
|
|
|
tsdbFlag := flag.String("tsdb", "",
|
|
|
|
"Address of the OpenTSDB server where to push telemetry to")
|
|
|
|
textFlag := flag.Bool("text", false,
|
|
|
|
"Print the output as simple text")
|
|
|
|
configFlag := flag.String("config", "",
|
|
|
|
"Config to turn OpenConfig telemetry into OpenTSDB put requests")
|
|
|
|
isUDPServerFlag := flag.Bool("isudpserver", false,
|
|
|
|
"Set to true to run as a UDP to TCP to OpenTSDB server.")
|
|
|
|
udpAddrFlag := flag.String("udpaddr", "",
|
|
|
|
"Address of the UDP server to connect to/serve on.")
|
|
|
|
parityFlag := flag.Int("parityshards", 0,
|
|
|
|
"Number of parity shards for the Reed Solomon Erasure Coding used for UDP."+
|
|
|
|
" Clients and servers should have the same number.")
|
|
|
|
udpTimeoutFlag := flag.Duration("udptimeout", 2*time.Second,
|
|
|
|
"Timeout for each")
|
|
|
|
|
2018-09-05 15:36:14 +00:00
|
|
|
flag.Parse()
|
2018-01-29 19:44:18 +00:00
|
|
|
if !(*tsdbFlag != "" || *textFlag || *udpAddrFlag != "") {
|
|
|
|
glog.Fatal("Specify the address of the OpenTSDB server to write to with -tsdb")
|
|
|
|
} else if *configFlag == "" {
|
|
|
|
glog.Fatal("Specify a JSON configuration file with -config")
|
|
|
|
}
|
|
|
|
|
|
|
|
config, err := loadConfig(*configFlag)
|
|
|
|
if err != nil {
|
|
|
|
glog.Fatal(err)
|
|
|
|
}
|
|
|
|
// Ignore the default "subscribe-to-everything" subscription of the
|
|
|
|
// -subscribe flag.
|
2018-09-05 15:36:14 +00:00
|
|
|
subscriptions := strings.Split(*subscribePaths, ",")
|
2018-01-29 19:44:18 +00:00
|
|
|
if subscriptions[0] == "" {
|
|
|
|
subscriptions = subscriptions[1:]
|
|
|
|
}
|
|
|
|
// Add the subscriptions from the config file.
|
|
|
|
subscriptions = append(subscriptions, config.Subscriptions...)
|
|
|
|
|
|
|
|
// Run a UDP server that forwards messages to OpenTSDB via Telnet (TCP)
|
|
|
|
if *isUDPServerFlag {
|
|
|
|
if *udpAddrFlag == "" {
|
|
|
|
glog.Fatal("Specify the address for the UDP server to listen on with -udpaddr")
|
|
|
|
}
|
|
|
|
server, err := newUDPServer(*udpAddrFlag, *tsdbFlag, *parityFlag)
|
|
|
|
if err != nil {
|
|
|
|
glog.Fatal("Failed to create UDP server: ", err)
|
|
|
|
}
|
|
|
|
glog.Fatal(server.Run())
|
|
|
|
}
|
|
|
|
|
|
|
|
var c OpenTSDBConn
|
|
|
|
if *textFlag {
|
|
|
|
c = newTextDumper()
|
|
|
|
} else if *udpAddrFlag != "" {
|
|
|
|
c = newUDPClient(*udpAddrFlag, *parityFlag, *udpTimeoutFlag)
|
|
|
|
} else {
|
|
|
|
// TODO: support HTTP(S).
|
|
|
|
c = newTelnetClient(*tsdbFlag)
|
|
|
|
}
|
2018-09-05 15:36:14 +00:00
|
|
|
ctx := gnmi.NewContext(context.Background(), cfg)
|
|
|
|
client, err := gnmi.Dial(cfg)
|
|
|
|
if err != nil {
|
|
|
|
glog.Fatal(err)
|
|
|
|
}
|
|
|
|
respChan := make(chan *pb.SubscribeResponse)
|
|
|
|
errChan := make(chan error)
|
|
|
|
subscribeOptions := &gnmi.SubscribeOptions{
|
|
|
|
Mode: "stream",
|
|
|
|
StreamMode: "target_defined",
|
|
|
|
Paths: gnmi.SplitPaths(subscriptions),
|
|
|
|
}
|
|
|
|
go gnmi.Subscribe(ctx, client, subscribeOptions, respChan, errChan)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case resp := <-respChan:
|
|
|
|
pushToOpenTSDB(cfg.Addr, c, config, resp.GetUpdate())
|
|
|
|
case err := <-errChan:
|
|
|
|
glog.Fatal(err)
|
2018-01-29 19:44:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-05 15:36:14 +00:00
|
|
|
func pushToOpenTSDB(addr string, conn OpenTSDBConn, config *Config, notif *pb.Notification) {
|
|
|
|
if notif == nil {
|
|
|
|
glog.Error("Nil notification ignored")
|
|
|
|
return
|
|
|
|
}
|
2018-01-29 19:44:18 +00:00
|
|
|
if notif.Timestamp <= 0 {
|
|
|
|
glog.Fatalf("Invalid timestamp %d in %s", notif.Timestamp, notif)
|
|
|
|
}
|
|
|
|
host := addr[:strings.IndexRune(addr, ':')]
|
|
|
|
if host == "localhost" {
|
|
|
|
// TODO: On Linux this reads /proc/sys/kernel/hostname each time,
|
|
|
|
// which isn't the most efficient, but at least we don't have to
|
|
|
|
// deal with detecting hostname changes.
|
|
|
|
host, _ = os.Hostname()
|
|
|
|
if host == "" {
|
|
|
|
glog.Info("could not figure out localhost's hostname")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prefix := "/" + strings.Join(notif.Prefix.Element, "/")
|
|
|
|
for _, update := range notif.Update {
|
2018-09-05 15:36:14 +00:00
|
|
|
if update.Value == nil || update.Value.Type != pb.Encoding_JSON {
|
2018-01-29 19:44:18 +00:00
|
|
|
glog.V(9).Infof("Ignoring incompatible update value in %s", update)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
value := parseValue(update)
|
|
|
|
if value == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
path := prefix + "/" + strings.Join(update.Path.Element, "/")
|
|
|
|
metricName, tags := config.Match(path)
|
|
|
|
if metricName == "" {
|
|
|
|
glog.V(8).Infof("Ignoring unmatched update at %s: %+v", path, update.Value)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
tags["host"] = host
|
|
|
|
for i, v := range value {
|
|
|
|
if len(value) > 1 {
|
|
|
|
tags["index"] = strconv.Itoa(i)
|
|
|
|
}
|
|
|
|
err := conn.Put(&DataPoint{
|
|
|
|
Metric: metricName,
|
|
|
|
Timestamp: uint64(notif.Timestamp),
|
|
|
|
Value: v,
|
|
|
|
Tags: tags,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
glog.Info("Failed to put datapoint: ", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseValue returns either an integer/floating point value of the given update, or if
|
|
|
|
// the value is a slice of integers/floating point values. If the value is neither of these
|
|
|
|
// or if any element in the slice is non numerical, parseValue returns nil.
|
2018-09-05 15:36:14 +00:00
|
|
|
func parseValue(update *pb.Update) []interface{} {
|
2018-01-29 19:44:18 +00:00
|
|
|
var value interface{}
|
|
|
|
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(update.Value.Value))
|
|
|
|
decoder.UseNumber()
|
|
|
|
err := decoder.Decode(&value)
|
|
|
|
if err != nil {
|
|
|
|
glog.Fatalf("Malformed JSON update %q in %s", update.Value.Value, update)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch value := value.(type) {
|
|
|
|
case json.Number:
|
|
|
|
return []interface{}{parseNumber(value, update)}
|
|
|
|
case []interface{}:
|
|
|
|
for i, val := range value {
|
|
|
|
jsonNum, ok := val.(json.Number)
|
|
|
|
if !ok {
|
|
|
|
// If any value is not a number, skip it.
|
|
|
|
glog.Infof("Element %d: %v is %T, not json.Number", i, val, val)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
num := parseNumber(jsonNum, update)
|
|
|
|
value[i] = num
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
case map[string]interface{}:
|
|
|
|
// Special case for simple value types that just have a "value"
|
|
|
|
// attribute (common case).
|
|
|
|
if val, ok := value["value"].(json.Number); ok && len(value) == 1 {
|
|
|
|
return []interface{}{parseNumber(val, update)}
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
glog.V(9).Infof("Ignoring non-numeric or non-numeric slice value in %s", update)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert our json.Number to either an int64, uint64, or float64.
|
2018-09-05 15:36:14 +00:00
|
|
|
func parseNumber(num json.Number, update *pb.Update) interface{} {
|
2018-01-29 19:44:18 +00:00
|
|
|
var value interface{}
|
|
|
|
var err error
|
|
|
|
if value, err = num.Int64(); err != nil {
|
|
|
|
// num is either a large unsigned integer or a floating point.
|
|
|
|
if strings.Contains(err.Error(), "value out of range") { // Sigh.
|
|
|
|
value, err = strconv.ParseUint(num.String(), 10, 64)
|
|
|
|
} else {
|
|
|
|
value, err = num.Float64()
|
|
|
|
if err != nil {
|
|
|
|
glog.Fatalf("Malformed JSON number %q in %s", num, update)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
}
|