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 }