diff --git a/controller/controller.go b/controller/controller.go index 5bab7529..2c4912b3 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -2,6 +2,7 @@ package controller import ( "github.com/go-openapi/loads" + influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/openziti-test-kitchen/zrok/controller/store" "github.com/openziti-test-kitchen/zrok/rest_server_zrok" "github.com/openziti-test-kitchen/zrok/rest_server_zrok/operations" @@ -13,6 +14,7 @@ import ( var cfg *Config var str *store.Store var mtr *metricsAgent +var idb influxdb2.Client const version = "v0.2.0" @@ -47,6 +49,10 @@ func Run(inCfg *Config) error { return errors.Wrap(err, "error opening store") } + if cfg.Influx != nil { + idb = influxdb2.NewClient(cfg.Influx.Url, cfg.Influx.Token) + } + if cfg.Metrics != nil { mtr = newMetricsAgent() go mtr.run() diff --git a/controller/metrics.go b/controller/metrics.go index c40495e9..daa5b066 100644 --- a/controller/metrics.go +++ b/controller/metrics.go @@ -21,7 +21,6 @@ import ( ) type metricsAgent struct { - influx influxdb2.Client writeApi api.WriteAPIBlocking metricsQueue chan *model.Metrics envCache map[string]*envCacheEntry @@ -43,9 +42,8 @@ func newMetricsAgent() *metricsAgent { shutdown: make(chan struct{}), joined: make(chan struct{}), } - if cfg.Influx != nil { - ma.influx = influxdb2.NewClient(cfg.Influx.Url, cfg.Influx.Token) - ma.writeApi = ma.influx.WriteAPIBlocking(cfg.Influx.Org, cfg.Influx.Bucket) + if idb != nil { + ma.writeApi = idb.WriteAPIBlocking(cfg.Influx.Org, cfg.Influx.Bucket) } return ma } diff --git a/controller/overview.go b/controller/overview.go index d2454142..533d24d9 100644 --- a/controller/overview.go +++ b/controller/overview.go @@ -1,7 +1,10 @@ package controller import ( + "context" + "fmt" "github.com/go-openapi/runtime/middleware" + "github.com/openziti-test-kitchen/zrok/controller/store" "github.com/openziti-test-kitchen/zrok/rest_model_zrok" "github.com/openziti-test-kitchen/zrok/rest_server_zrok/operations/metadata" "github.com/sirupsen/logrus" @@ -26,31 +29,90 @@ func overviewHandler(_ metadata.OverviewParams, principal *rest_model_zrok.Princ logrus.Errorf("error finding services for environment '%v': %v", env.ZId, err) return metadata.NewOverviewInternalServerError() } - if env.Active { - es := &rest_model_zrok.EnvironmentServices{ - Environment: &rest_model_zrok.Environment{ - Address: env.Address, - CreatedAt: env.CreatedAt.String(), - Description: env.Description, - Host: env.Host, - UpdatedAt: env.UpdatedAt.String(), - ZID: env.ZId, - }, - } - for _, svc := range svcs { - if svc.Active { - es.Services = append(es.Services, &rest_model_zrok.Service{ - CreatedAt: svc.CreatedAt.String(), - Frontend: svc.Frontend, - Backend: svc.Backend, - UpdatedAt: svc.UpdatedAt.String(), - ZID: svc.ZId, - Name: svc.Name, - }) - } - } - out = append(out, es) + es := &rest_model_zrok.EnvironmentServices{ + Environment: &rest_model_zrok.Environment{ + Address: env.Address, + CreatedAt: env.CreatedAt.String(), + Description: env.Description, + Host: env.Host, + UpdatedAt: env.UpdatedAt.String(), + ZID: env.ZId, + }, } + sparkData, err := sparkDataForServices(svcs) + if err != nil { + logrus.Errorf("error querying spark data for services: %v", err) + return metadata.NewOverviewInternalServerError() + } + for _, svc := range svcs { + es.Services = append(es.Services, &rest_model_zrok.Service{ + CreatedAt: svc.CreatedAt.String(), + Frontend: svc.Frontend, + Backend: svc.Backend, + UpdatedAt: svc.UpdatedAt.String(), + ZID: svc.ZId, + Name: svc.Name, + Metrics: sparkData[svc.Name], + }) + } + out = append(out, es) } return metadata.NewOverviewOK().WithPayload(out) } + +func sparkDataForServices(svcs []*store.Service) (map[string][]int64, error) { + out := make(map[string][]int64) + + if len(svcs) > 0 { + qapi := idb.QueryAPI(cfg.Influx.Org) + + result, err := qapi.Query(context.Background(), sparkFluxQuery(svcs)) + if err != nil { + return nil, err + } + + for result.Next() { + combinedRate := int64(0) + readRate := result.Record().ValueByKey("_value_t1") + if readRate != nil { + combinedRate += int64(readRate.(float64)) + } + writeRate := result.Record().ValueByKey("_value_t2") + if writeRate != nil { + combinedRate += int64(writeRate.(float64)) + } + svcName := result.Record().ValueByKey("service_t1").(string) + svcMetrics := out[svcName] + svcMetrics = append(svcMetrics, combinedRate) + out[svcName] = svcMetrics + } + } + return out, nil +} + +func sparkFluxQuery(svcs []*store.Service) string { + svcFilter := "|> filter(fn: (r) =>" + for i, svc := range svcs { + if i > 0 { + svcFilter += " or" + } + svcFilter += fmt.Sprintf(" r[\"service\"] == \"%v\"", svc.Name) + } + svcFilter += ")" + query := "read = from(bucket: \"zrok\")" + + "|> range(start: -5m)" + + "|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")" + + "|> filter(fn: (r) => r[\"_field\"] == \"bytesRead\")" + + "|> filter(fn: (r) => r[\"namespace\"] == \"frontend\")" + + svcFilter + + "|> aggregateWindow(every: 5s, fn: mean, createEmpty: true)\n\n" + + "written = from(bucket: \"zrok\")" + + "|> range(start: -5m)" + + "|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")" + + "|> filter(fn: (r) => r[\"_field\"] == \"bytesWritten\")" + + "|> filter(fn: (r) => r[\"namespace\"] == \"frontend\")" + + svcFilter + + "|> aggregateWindow(every: 5s, fn: mean, createEmpty: true)\n\n" + + "join(tables: {t1: read, t2: written}, on: [\"_time\"])" + return query +} diff --git a/controller/store/environment.go b/controller/store/environment.go index 2f3b75f7..216f8dc0 100644 --- a/controller/store/environment.go +++ b/controller/store/environment.go @@ -12,11 +12,10 @@ type Environment struct { Host string Address string ZId string - Active bool } func (self *Store) CreateEnvironment(accountId int, i *Environment, tx *sqlx.Tx) (int, error) { - stmt, err := tx.Prepare("insert into environments (account_id, description, host, address, z_id, active) values (?, ?, ?, ?, ?, true)") + stmt, err := tx.Prepare("insert into environments (account_id, description, host, address, z_id) values (?, ?, ?, ?, ?)") if err != nil { return 0, errors.Wrap(err, "error preparing environments insert statement") } diff --git a/controller/store/service.go b/controller/store/service.go index 137181eb..fa3e6909 100644 --- a/controller/store/service.go +++ b/controller/store/service.go @@ -12,11 +12,10 @@ type Service struct { Name string Frontend string Backend string - Active bool } func (self *Store) CreateService(envId int, svc *Service, tx *sqlx.Tx) (int, error) { - stmt, err := tx.Prepare("insert into services (environment_id, z_id, name, frontend, backend, active) values (?, ?, ?, ?, ?, true)") + stmt, err := tx.Prepare("insert into services (environment_id, z_id, name, frontend, backend) values (?, ?, ?, ?, ?)") if err != nil { return 0, errors.Wrap(err, "error preparing services insert statement") } @@ -72,12 +71,12 @@ func (self *Store) FindServicesForEnvironment(envId int, tx *sqlx.Tx) ([]*Servic } func (self *Store) UpdateService(svc *Service, tx *sqlx.Tx) error { - sql := "update services set z_id = ?, name = ?, frontend = ?, backend = ?, active = ?, updated_at = strftime('%Y-%m-%d %H:%M:%f', 'now') where id = ?" + sql := "update services set z_id = ?, name = ?, frontend = ?, backend = ?, updated_at = strftime('%Y-%m-%d %H:%M:%f', 'now') where id = ?" stmt, err := tx.Prepare(sql) if err != nil { return errors.Wrap(err, "error preparing services update statement") } - _, err = stmt.Exec(svc.ZId, svc.Name, svc.Frontend, svc.Backend, svc.Active, svc.Id) + _, err = stmt.Exec(svc.ZId, svc.Name, svc.Frontend, svc.Backend, svc.Id) if err != nil { return errors.Wrap(err, "error executing services update statement") } diff --git a/controller/store/sql/000_base.sql b/controller/store/sql/000_base.sql index 3640a59d..fb331149 100644 --- a/controller/store/sql/000_base.sql +++ b/controller/store/sql/000_base.sql @@ -38,7 +38,6 @@ create table environments ( host string, address string, z_id string not null unique, - active boolean not null, created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), @@ -55,7 +54,6 @@ create table services ( name string not null unique, frontend string, backend string, - active boolean not null, created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), diff --git a/controller/untunnel.go b/controller/untunnel.go index a0756059..13d260cb 100644 --- a/controller/untunnel.go +++ b/controller/untunnel.go @@ -101,8 +101,7 @@ func (self *untunnelHandler) Handle(params tunnel.UntunnelParams, principal *res logrus.Infof("deallocated service '%v'", svcName) - ssvc.Active = false - if err := str.UpdateService(ssvc, tx); err != nil { + if err := str.DeleteService(ssvc.Id, tx); err != nil { logrus.Errorf("error deactivating service '%v': %v", svcZId, err) return tunnel.NewUntunnelInternalServerError() } diff --git a/ui/package-lock.json b/ui/package-lock.json index 6fa742a7..bdccface 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -21,6 +21,7 @@ "react-flow-renderer": "^10.3.12", "react-router-dom": "^6.4.0", "react-scripts": "5.0.1", + "react-sparklines": "^1.7.0", "styled-components": "^5.3.5" } }, @@ -14401,6 +14402,18 @@ } } }, + "node_modules/react-sparklines": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz", + "integrity": "sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==", + "dependencies": { + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -27208,6 +27221,14 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-sparklines": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz", + "integrity": "sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==", + "requires": { + "prop-types": "^15.5.10" + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/ui/package.json b/ui/package.json index eede04ce..283bafc7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,7 @@ "react-flow-renderer": "^10.3.12", "react-router-dom": "^6.4.0", "react-scripts": "5.0.1", + "react-sparklines": "^1.7.0", "styled-components": "^5.3.5" }, "scripts": { diff --git a/ui/src/Environments.js b/ui/src/Environments.js index 2251b37f..bc5013ff 100644 --- a/ui/src/Environments.js +++ b/ui/src/Environments.js @@ -30,17 +30,8 @@ const Environments = (props) => { }, ] - const conditionalRowStyles = [ - { - when: row => !row.environment.active, - style: { - display: 'none' - } - } - ] - const servicesComponent = ({ data }) => - const servicesExpanded = row => row.services != null && row.services.length > 0 && row.services.some((row) => row.active) + const servicesExpanded = row => row.services != null && row.services.length > 0 return (
@@ -54,7 +45,6 @@ const Environments = (props) => { expandableRows expandableRowsComponent={servicesComponent} expandableRowExpanded={servicesExpanded} - conditionalRowStyles={conditionalRowStyles} />
)} diff --git a/ui/src/Network.js b/ui/src/Network.js index b74c3eda..cf132ad0 100644 --- a/ui/src/Network.js +++ b/ui/src/Network.js @@ -24,7 +24,7 @@ const Network = () => { reactFlow.fitView({maxZoom: 1}) } }); - }) + }, []) useEffect(() => { let mounted = true @@ -81,23 +81,21 @@ function buildGraph(overview) { id++ if(item.services != null) { item.services.forEach((item) => { - if(item.active) { - out.nodes.push({ - id: '' + id, - data: {label:
{ item.frontend }
}, - position: {x: (id * 25), y: 0}, - style: { width: 'fit-content', backgroundColor: '#9367ef', color: 'white' }, - type: 'output', - draggable: true - }) - out.edges.push({ - id: 'e' + envId + '-' + id, - source: '' + envId, - target: '' + id, - animated: true - }) - id++ - } + out.nodes.push({ + id: '' + id, + data: {label:
{ item.frontend }
}, + position: {x: (id * 25), y: 0}, + style: { width: 'fit-content', backgroundColor: '#9367ef', color: 'white' }, + type: 'output', + draggable: true + }) + out.edges.push({ + id: 'e' + envId + '-' + id, + source: '' + envId, + target: '' + id, + animated: true + }) + id++ }); } }); diff --git a/ui/src/Services.js b/ui/src/Services.js index 8782061d..b05d96e5 100644 --- a/ui/src/Services.js +++ b/ui/src/Services.js @@ -1,11 +1,7 @@ import DataTable from 'react-data-table-component'; -import {useEffect} from "react"; +import {Sparklines, SparklinesLine} from 'react-sparklines'; const Services = (props) => { - useEffect((props) => { - console.log(props) - }, []) - const columns = [ { name: 'Frontend', @@ -17,14 +13,9 @@ const Services = (props) => { selector: row => row.backend, sortable: true, }, - ] - - const conditionalRowStyles = [ { - when: row => !row.active, - style: { - display: 'none' - } + name: 'Activity', + cell: row => } ] @@ -35,7 +26,6 @@ const Services = (props) => { columns={columns} data={props.services} defaultSortFieldId={1} - conditionalRowStyles={conditionalRowStyles} /> )}