ipld-eth-server/vendor/github.com/aristanetworks/goarista/cmd/octsdb/main.go

229 lines
7.0 KiB
Go
Raw Normal View History

// 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"
"context"
"encoding/json"
"flag"
"os"
"strconv"
"strings"
"time"
"github.com/aristanetworks/goarista/gnmi"
"github.com/aristanetworks/glog"
pb "github.com/openconfig/gnmi/proto/gnmi"
)
func main() {
// 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")
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")
flag.Parse()
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.
subscriptions := strings.Split(*subscribePaths, ",")
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)
}
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)
}
}
}
func pushToOpenTSDB(addr string, conn OpenTSDBConn, config *Config, notif *pb.Notification) {
if notif == nil {
glog.Error("Nil notification ignored")
return
}
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 {
if update.Value == nil || update.Value.Type != pb.Encoding_JSON {
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.
func parseValue(update *pb.Update) []interface{} {
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.
func parseNumber(num json.Number, update *pb.Update) interface{} {
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
}