diff --git a/go.mod b/go.mod index 798348e7a..8614c8a05 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/golang/protobuf v1.3.5 github.com/google/uuid v1.1.1 github.com/gorilla/mux v1.7.3 + github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19 github.com/michaelquigley/pfxlog v0.0.0-20190813191113-2be43bd0dccc diff --git a/ziti-probe/README.md b/ziti-probe/README.md new file mode 100644 index 000000000..d702a6552 --- /dev/null +++ b/ziti-probe/README.md @@ -0,0 +1,61 @@ +Ziti Probe +---------- + +`ziti-probe` is utility application allowing to collect network metrics from the _edge_. + +## Setup + +create config type for metric collection service with the following schema (this is optional) +```json +{ + "$id": "http://ziti-edge.netfoundry.io/schemas/ziti-probe-config.v1.json", + "type":"object", + "additionalProperties": false, + "required": [], + "properties":{ + "dbType": { + "type":"string" + }, + "dbName": { + "type": "string" + }, + "interval": { + "type": "integer", + "minimum": 1, + "maximum": 3600 + } + } +} +``` +create service configuration `probe-service-cfg` (exact name does not matter, +it wil be used to configure the service in the next step), This is optional +```js +{ + "dbType": "influxdb", // only influxdb is currently supported + "dbName": "ziti", // name of database in Influxdb, ziti is the default + "interval": 60 // how often metrics are uploaded (in seconds), 60 is the default +} +``` +create service with a router termination +```shell script +$ ziti edge controller create service probe-service probe-service-cfg +$ ziti edge controller create terminator probe-service router1 tcp:127.0.0.1:8086 +``` +in this example `router1` is the name of the fabric router that has access to influxDB server, +and `tcp:127.0.0.1:8086` is the address of influxDB server. Adjust as needed. + +create ziti identity(ies) for your probe(s). `ziti-probe` is a Ziti-SDK-enabled application, +so standard enrollment procedure should be followed. + +## Running `ziti-probe` + +```shell script +$ ziti-probe run +``` + #### What's happening? + 1. `ziti-probe` authenticates to Ziti controller with provided identity + 1. it checks that `probe-service` is available and get configuration. If configuration is not assigned default values are used. + 1. it opens connections to all edge routers configured for `probe-service` (refer to ziti policies) + 1. each connection performs a round-trip latency probe to each of edge routers every 30 seconds + 1. `ziti-probe` then submits collected metrics to `probe-service`. + metric names are in the following format: `latency.`, e.g `latency.tls://54.145.23.14:3022` diff --git a/ziti-probe/subcmd/root.go b/ziti-probe/subcmd/root.go new file mode 100644 index 000000000..2a01207a6 --- /dev/null +++ b/ziti-probe/subcmd/root.go @@ -0,0 +1,60 @@ +/* + Copyright NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package subcmd + +import ( + "fmt" + "github.com/michaelquigley/pfxlog" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func init() { + root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") + root.PersistentFlags().StringVar(&logFormatter, "log-formatter", "", "Specify log formatter [json|pfxlog|text]") +} + +var root = &cobra.Command{ + Use: "ziti-probe", + Short: "Ziti Probe", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if verbose { + logrus.SetLevel(logrus.DebugLevel) + } + + switch logFormatter { + case "pfxlog": + logrus.SetFormatter(pfxlog.NewFormatterStartingToday()) + case "json": + logrus.SetFormatter(&logrus.JSONFormatter{}) + case "text": + logrus.SetFormatter(&logrus.TextFormatter{}) + default: + // let logrus do its own thing + } + + }, +} + +var verbose bool +var logFormatter string + +func Execute() { + if err := root.Execute(); err != nil { + fmt.Printf("error: %s\n", err) + } +} diff --git a/ziti-probe/subcmd/run.go b/ziti-probe/subcmd/run.go new file mode 100644 index 000000000..3e60c73c1 --- /dev/null +++ b/ziti-probe/subcmd/run.go @@ -0,0 +1,158 @@ +package subcmd + +import ( + "context" + influxdb "github.com/influxdata/influxdb1-client" + "github.com/michaelquigley/pfxlog" + "github.com/netfoundry/ziti-foundation/metrics" + "github.com/netfoundry/ziti-foundation/metrics/metrics_pb" + "github.com/netfoundry/ziti-sdk-golang/ziti" + "github.com/netfoundry/ziti-sdk-golang/ziti/config" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "net" + "net/http" + "net/url" + "os" + "reflect" + "sync/atomic" + "time" + "unsafe" +) + +var runCmd = &cobra.Command{ + Use: "run ", + Short: "Runs ziti probe", + Long: "runs ziti probe: captures latency to edge routers and submits them to probe-service", + Args: cobra.MaximumNArgs(1), + Run: theProbe.run, +} + +func init() { + root.AddCommand(runCmd) +} + +var log = pfxlog.Logger() + +type probeCfg struct { + dbName string + dbType string + interval int +} + +var defaultConfig = probeCfg{ + dbName: "ziti", + dbType: "influxdb", + interval: 5 * 60, // 5 minutes +} + +type probe struct{ + cfg *probeCfg + indb *influxdb.Client + ctx ziti.Context + latest atomic.Value + closer chan interface{} +} + +var theProbe = &probe{ + cfg: &defaultConfig, + closer: make(chan interface{}), +} + +func (p *probe) AcceptMetrics(message *metrics_pb.MetricsMessage) { + p.latest.Store(message) +} + +func (p *probe) sendMetrics() { + for { + select { + case <-time.After(time.Duration(p.cfg.interval) * time.Second): + message := p.latest.Load().(*metrics_pb.MetricsMessage) + if message == nil { + continue + } + bp, err := metrics.AsBatch(message) + if err != nil { + logrus.Errorln(err) + return + } + bp.Database = p.cfg.dbName + _, err = p.indb.Write(*bp) + if err != nil { + logrus.Errorln(err) + } + } + } +} + +func (p *probe) run(cmd *cobra.Command, args []string) { + + var err error + if len(args) == 0 { + log.Infof("loading ziti identity from env.ZITI_SDK_CONFIG[%s]", os.Getenv("ZITI_SDK_CONFIG")) + p.ctx = ziti.NewContext() + } else { + if cfg, err := config.NewFromFile(args[0]); err != nil { + log.Fatalf("failed to load config from file[%s]", args[0], err) + } else { + p.ctx = ziti.NewContextWithConfig(cfg) + } + + } + + _, _ = p.ctx.GetServices() + + _, found := p.ctx.GetService("probe-service") + if !found { + log.Fatal("required service was not found") + } + + p.indb, err = p.createInfluxDbClient() + if err != nil { + log.Error(err) + return + } + _, res, err := p.indb.Ping() + if err != nil { + log.Fatalf("failed to get server info", err) + } + log.Info("connected to influx version = ", res) + + p.ctx.Metrics().EventController().AddHandler(p) + + go p.sendMetrics() + + select { + case <-p.closer: + } + + p.ctx.Close() +} + +func (p *probe) createInfluxDbClient() (*influxdb.Client, error) { + + httpC := &http.Client{ + Transport: &http.Transport{ + DialContext: func (c context.Context, network, addr string) (net.Conn, error) { + log.Infof("connecting to %s:%s", network, addr) + return p.ctx.Dial("probe-service") + }, + }, + } + + config := influxdb.NewConfig() + u, _ := url.Parse("http://probe-service") + config.URL = *u + clt, _ := influxdb.NewClient(config) + + ic := reflect.ValueOf(clt) + ict := ic.Type() + hcf, _ := ict.Elem().FieldByName("httpClient") + + hcp := unsafe.Pointer(uintptr(unsafe.Pointer(clt)) + hcf.Offset) + + hcpp := (**http.Client)(hcp) + *hcpp = httpC + + return clt, nil +} diff --git a/ziti-probe/ziti-probe.go b/ziti-probe/ziti-probe.go new file mode 100644 index 000000000..b9129a341 --- /dev/null +++ b/ziti-probe/ziti-probe.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/michaelquigley/pfxlog" + "github.com/netfoundry/ziti-cmd/ziti-probe/subcmd" + "github.com/netfoundry/ziti-foundation/util/debugz" + "github.com/sirupsen/logrus" +) + +func init() { + pfxlog.Global(logrus.InfoLevel) + pfxlog.SetPrefix("github.com/netfoundry/") +} + +func main() { + debugz.AddStackDumpHandler() + subcmd.Execute() +}