github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/cmd/octsdb/main.go (about)

     1  // Copyright (c) 2016 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 octsdb tool pushes OpenConfig telemetry to OpenTSDB.
     6  package main
     7  
     8  import (
     9  	"context"
    10  	"encoding/json"
    11  	"flag"
    12  	"fmt"
    13  	"math"
    14  	"os"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/aristanetworks/goarista/gnmi"
    20  
    21  	"github.com/aristanetworks/glog"
    22  	pb "github.com/openconfig/gnmi/proto/gnmi"
    23  	"golang.org/x/sync/errgroup"
    24  )
    25  
    26  func main() {
    27  
    28  	// gNMI options
    29  	cfg := &gnmi.Config{}
    30  	flag.StringVar(&cfg.Addr, "addr", "localhost", "gNMI gRPC server `address`")
    31  	flag.StringVar(&cfg.CAFile, "cafile", "", "Path to server TLS certificate file")
    32  	flag.StringVar(&cfg.CertFile, "certfile", "", "Path to client TLS certificate file")
    33  	flag.StringVar(&cfg.KeyFile, "keyfile", "", "Path to client TLS private key file")
    34  	flag.StringVar(&cfg.Username, "username", "", "Username to authenticate with")
    35  	flag.StringVar(&cfg.Password, "password", "", "Password to authenticate with")
    36  	flag.BoolVar(&cfg.TLS, "tls", false, "Enable TLS")
    37  	flag.StringVar(&cfg.TLSMinVersion, "tls-min-version", "",
    38  		fmt.Sprintf("Set minimum TLS version for connection (%s)", gnmi.TLSVersions))
    39  	flag.StringVar(&cfg.TLSMaxVersion, "tls-max-version", "",
    40  		fmt.Sprintf("Set maximum TLS version for connection (%s)", gnmi.TLSVersions))
    41  
    42  	// Program options
    43  	subscribePaths := flag.String("paths", "", "Comma-separated list of paths to subscribe to")
    44  
    45  	tsdbFlag := flag.String("tsdb", "",
    46  		"Address of the OpenTSDB server where to push telemetry to")
    47  	textFlag := flag.Bool("text", false,
    48  		"Print the output as simple text")
    49  	configFlag := flag.String("config", "",
    50  		"Config to turn OpenConfig telemetry into OpenTSDB put requests")
    51  	isUDPServerFlag := flag.Bool("isudpserver", false,
    52  		"Set to true to run as a UDP to TCP to OpenTSDB server.")
    53  	udpAddrFlag := flag.String("udpaddr", "",
    54  		"Address of the UDP server to connect to/serve on.")
    55  	parityFlag := flag.Int("parityshards", 0,
    56  		"Number of parity shards for the Reed Solomon Erasure Coding used for UDP."+
    57  			" Clients and servers should have the same number.")
    58  	udpTimeoutFlag := flag.Duration("udptimeout", 2*time.Second,
    59  		"Timeout for each")
    60  
    61  	flag.Parse()
    62  	if !(*tsdbFlag != "" || *textFlag || *udpAddrFlag != "") {
    63  		glog.Fatal("Specify the address of the OpenTSDB server to write to with -tsdb")
    64  	} else if *configFlag == "" {
    65  		glog.Fatal("Specify a JSON configuration file with -config")
    66  	}
    67  
    68  	config, err := loadConfig(*configFlag)
    69  	if err != nil {
    70  		glog.Fatal(err)
    71  	}
    72  	var subscriptions []string
    73  	if *subscribePaths != "" {
    74  		subscriptions = strings.Split(*subscribePaths, ",")
    75  	}
    76  	// Add the subscriptions from the config file.
    77  	subscriptions = append(subscriptions, config.Subscriptions...)
    78  
    79  	// Run a UDP server that forwards messages to OpenTSDB via Telnet (TCP)
    80  	if *isUDPServerFlag {
    81  		if *udpAddrFlag == "" {
    82  			glog.Fatal("Specify the address for the UDP server to listen on with -udpaddr")
    83  		}
    84  		server, err := newUDPServer(*udpAddrFlag, *tsdbFlag, *parityFlag)
    85  		if err != nil {
    86  			glog.Fatal("Failed to create UDP server: ", err)
    87  		}
    88  		glog.Fatal(server.Run())
    89  	}
    90  
    91  	var c OpenTSDBConn
    92  	if *textFlag {
    93  		c = newTextDumper()
    94  	} else if *udpAddrFlag != "" {
    95  		c = newUDPClient(*udpAddrFlag, *parityFlag, *udpTimeoutFlag)
    96  	} else {
    97  		// TODO: support HTTP(S).
    98  		c = newTelnetClient(*tsdbFlag)
    99  	}
   100  	ctx := gnmi.NewContext(context.Background(), cfg)
   101  	client, err := gnmi.Dial(cfg)
   102  	if err != nil {
   103  		glog.Fatal(err)
   104  	}
   105  	respChan := make(chan *pb.SubscribeResponse)
   106  	subscribeOptions := &gnmi.SubscribeOptions{
   107  		Mode:       "stream",
   108  		StreamMode: "target_defined",
   109  		Paths:      gnmi.SplitPaths(subscriptions),
   110  	}
   111  	var g errgroup.Group
   112  	g.Go(func() error { return gnmi.SubscribeErr(ctx, client, subscribeOptions, respChan) })
   113  	for resp := range respChan {
   114  		pushToOpenTSDB(cfg.Addr, c, config, resp.GetUpdate())
   115  	}
   116  	if err := g.Wait(); err != nil {
   117  		glog.Fatal(err)
   118  	}
   119  }
   120  
   121  func pushToOpenTSDB(addr string, conn OpenTSDBConn, config *Config, notif *pb.Notification) {
   122  	if notif == nil {
   123  		glog.Error("Nil notification ignored")
   124  		return
   125  	}
   126  	if notif.Timestamp <= 0 {
   127  		glog.Fatalf("Invalid timestamp %d in %s", notif.Timestamp, notif)
   128  	}
   129  	host := addr[:strings.IndexRune(addr, ':')]
   130  	if host == "localhost" {
   131  		// TODO: On Linux this reads /proc/sys/kernel/hostname each time,
   132  		// which isn't the most efficient, but at least we don't have to
   133  		// deal with detecting hostname changes.
   134  		host, _ = os.Hostname()
   135  		if host == "" {
   136  			glog.Info("could not figure out localhost's hostname")
   137  			return
   138  		}
   139  	}
   140  	prefix := gnmi.StrPath(notif.Prefix)
   141  	for _, update := range notif.Update {
   142  		path := prefix + gnmi.StrPath(update.Path)
   143  		metricName, tags, staticValueMap := config.Match(path)
   144  		if metricName == "" {
   145  			glog.V(8).Infof("Ignoring unmatched update at %s ", path)
   146  			continue
   147  		}
   148  		value := parseValue(update, staticValueMap)
   149  		if value == nil {
   150  			continue
   151  		}
   152  		tags["host"] = host
   153  		for i, v := range value {
   154  			if len(value) > 1 {
   155  				tags["index"] = strconv.Itoa(i)
   156  			}
   157  			err := conn.Put(&DataPoint{
   158  				Metric:    metricName,
   159  				Timestamp: uint64(notif.Timestamp),
   160  				Value:     v,
   161  				Tags:      tags,
   162  			})
   163  			if err != nil {
   164  				glog.Info("Failed to put datapoint: ", err)
   165  			}
   166  		}
   167  	}
   168  }
   169  
   170  // parseValue returns either an integer/floating point value of the given update, or if
   171  // the value is a slice of integers/floating point values. If the value is neither of these
   172  // or if any element in the slice is non numerical, parseValue returns nil.
   173  func parseValue(update *pb.Update, staticValueMap map[string]int64) []interface{} {
   174  	value, err := gnmi.ExtractValue(update)
   175  	if err != nil {
   176  		glog.Fatalf("Malformed JSON update %q in %s", update.Val.GetJsonVal(), update)
   177  	}
   178  
   179  	switch value := value.(type) {
   180  	case int64:
   181  		return []interface{}{value}
   182  	case uint64:
   183  		return []interface{}{value}
   184  	case float32, float64:
   185  		return []interface{}{value}
   186  	case *pb.Decimal64:
   187  		val := gnmi.DecimalToFloat(value)
   188  		if math.IsInf(val, 0) || math.IsNaN(val) {
   189  			return nil
   190  		}
   191  		return []interface{}{val}
   192  	case json.Number:
   193  		return []interface{}{parseNumber(value, update)}
   194  	case []interface{}:
   195  		for i, val := range value {
   196  			switch val := val.(type) {
   197  			case int64:
   198  				value[i] = val
   199  			case uint64:
   200  				value[i] = val
   201  			case float32, float64:
   202  				value[i] = val
   203  			case *pb.Decimal64:
   204  				v := gnmi.DecimalToFloat(val)
   205  				if math.IsInf(v, 0) || math.IsNaN(v) {
   206  					value[i] = nil
   207  				}
   208  				value[i] = v
   209  			case json.Number:
   210  				value[i] = parseNumber(val, update)
   211  			case map[string]interface{}:
   212  				if num, ok := val["value"].(json.Number); ok && len(val) == 1 {
   213  					value[i] = parseNumber(num, update)
   214  				}
   215  			case string:
   216  				return parseString(val, staticValueMap)
   217  			default:
   218  				// If any value is not a number, skip it.
   219  				glog.V(3).Infof("Element %d: %v is %T, not json.Number", i, val, val)
   220  				continue
   221  			}
   222  		}
   223  		return value
   224  	case map[string]interface{}:
   225  		// Special case for simple value types that just have a "value"
   226  		// attribute (common case).
   227  		if val, ok := value["value"].(json.Number); ok && len(value) == 1 {
   228  			return []interface{}{parseNumber(val, update)}
   229  		}
   230  	case string:
   231  		return parseString(value, staticValueMap)
   232  
   233  	default:
   234  		glog.V(9).Infof("Ignoring non-numeric or non-numeric slice value in %s", update)
   235  	}
   236  	return nil
   237  }
   238  
   239  func parseString(value string, staticValueMap map[string]int64) []interface{} {
   240  	if newval, ok := staticValueMap[value]; ok {
   241  		return []interface{}{newval}
   242  	} else if newval, ok := staticValueMap["default"]; ok {
   243  		return []interface{}{newval}
   244  	} else {
   245  		return nil
   246  	}
   247  }
   248  
   249  // Convert our json.Number to either an int64, uint64, or float64.
   250  func parseNumber(num json.Number, update *pb.Update) interface{} {
   251  	var value interface{}
   252  	var err error
   253  	if value, err = num.Int64(); err != nil {
   254  		// num is either a large unsigned integer or a floating point.
   255  		if strings.Contains(err.Error(), "value out of range") { // Sigh.
   256  			value, err = strconv.ParseUint(num.String(), 10, 64)
   257  		} else {
   258  			value, err = num.Float64()
   259  			if err != nil {
   260  				glog.Fatalf("Malformed JSON number %q in %s", num, update)
   261  			}
   262  		}
   263  	}
   264  	return value
   265  }