From 701548c5b25efba70c3818e96a4394701cfb913e Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Sat, 13 Jul 2019 10:21:53 +0200 Subject: Add LowCollector & rename Supervisor to Monitoring Change-Id: Ibd78f1200ed71b0100ebd48dddb9236377e2fb36 Signed-off-by: Sebastien Douheret --- .vscode/settings.json | 1 + Makefile | 2 +- lib/agent/agent.go | 15 +- lib/agent/apiv1-monitoring.go | 214 ++++++++++++ lib/agent/apiv1-supervisor.go | 114 ------- lib/agent/apiv1.go | 12 +- lib/agent/xds-low-collector.go | 321 ++++++++++++++++++ lib/agent/xdsmonitoring.go | 217 ++++++++++++ lib/agent/xdssupervior.go | 230 ------------- lib/aglafb/afb.go | 53 +++ lib/xdsconfig/config.go | 8 +- lib/xdsconfig/configfile.go | 12 +- .../services/@core-xds-services.module.ts | 4 +- .../app/@core-xds/services/monitoring.service.ts | 106 ++++++ .../app/@core-xds/services/supervision.service.ts | 61 ---- .../src/app/@core-xds/services/xdsagent.service.ts | 33 +- .../config-global/config-global.component.html | 2 +- .../monitoring/monitoring-config.component.html | 77 +++++ .../monitoring/monitoring-config.component.scss | 153 +++++++++ .../monitoring/monitoring-config.component.ts | 362 +++++++++++++++++++++ .../app/pages/monitoring/monitoring.component.html | 69 ++++ .../app/pages/monitoring/monitoring.component.scss | 83 +++++ .../app/pages/monitoring/monitoring.component.ts | 172 ++++++++++ .../src/app/pages/monitoring/monitoring.module.ts | 37 +++ webapp/src/app/pages/pages-menu.ts | 8 +- webapp/src/app/pages/pages-routing.module.ts | 12 +- webapp/src/app/pages/pages.module.ts | 4 +- .../supervision/supervision-config.component.html | 45 --- .../supervision/supervision-config.component.scss | 140 -------- .../supervision/supervision-config.component.ts | 305 ----------------- .../pages/supervision/supervision.component.html | 69 ---- .../pages/supervision/supervision.component.scss | 83 ----- .../app/pages/supervision/supervision.component.ts | 174 ---------- .../app/pages/supervision/supervision.module.ts | 37 --- webapp/tslint.json | 13 +- 35 files changed, 1946 insertions(+), 1302 deletions(-) create mode 100644 lib/agent/apiv1-monitoring.go delete mode 100644 lib/agent/apiv1-supervisor.go create mode 100644 lib/agent/xds-low-collector.go create mode 100644 lib/agent/xdsmonitoring.go delete mode 100644 lib/agent/xdssupervior.go create mode 100644 lib/aglafb/afb.go create mode 100644 webapp/src/app/@core-xds/services/monitoring.service.ts delete mode 100644 webapp/src/app/@core-xds/services/supervision.service.ts create mode 100644 webapp/src/app/pages/monitoring/monitoring-config.component.html create mode 100644 webapp/src/app/pages/monitoring/monitoring-config.component.scss create mode 100644 webapp/src/app/pages/monitoring/monitoring-config.component.ts create mode 100644 webapp/src/app/pages/monitoring/monitoring.component.html create mode 100644 webapp/src/app/pages/monitoring/monitoring.component.scss create mode 100644 webapp/src/app/pages/monitoring/monitoring.component.ts create mode 100644 webapp/src/app/pages/monitoring/monitoring.module.ts delete mode 100644 webapp/src/app/pages/supervision/supervision-config.component.html delete mode 100644 webapp/src/app/pages/supervision/supervision-config.component.scss delete mode 100644 webapp/src/app/pages/supervision/supervision-config.component.ts delete mode 100644 webapp/src/app/pages/supervision/supervision.component.html delete mode 100644 webapp/src/app/pages/supervision/supervision.component.scss delete mode 100644 webapp/src/app/pages/supervision/supervision.component.ts delete mode 100644 webapp/src/app/pages/supervision/supervision.module.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 59cafea..84bb4a3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -56,6 +56,7 @@ "WSID", "XDSSUPERV", "abortinstall", + "aglafb", "apiv", "cmdi", "conv", diff --git a/Makefile b/Makefile index 273458f..fe8c9ec 100644 --- a/Makefile +++ b/Makefile @@ -149,7 +149,7 @@ else @echo "Skipping webapp build (SKIP_WEBAPP_BUILD var is set)" endif -webapp/debug: +webapp/debug dev-frontend devf: cd webapp && npm run watch webapp/install: diff --git a/lib/agent/agent.go b/lib/agent/agent.go index 506976d..e90c1a0 100644 --- a/lib/agent/agent.go +++ b/lib/agent/agent.go @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2018 "IoT.bzh" + * Copyright (C) 2017-2019 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -47,12 +47,13 @@ type Context struct { SThgCmd *exec.Cmd SThgInotCmd *exec.Cmd - webServer *WebServer - xdsServers map[string]*XdsServer - XdsSupervisor *XdsSupervisor - sessions *Sessions - events *Events - projects *Projects + webServer *WebServer + xdsServers map[string]*XdsServer + XdsMonitoring *XdsMonitoring + XdsLowCollector *XdsLowCollector + sessions *Sessions + events *Events + projects *Projects Exit chan os.Signal } diff --git a/lib/agent/apiv1-monitoring.go b/lib/agent/apiv1-monitoring.go new file mode 100644 index 0000000..cf651ce --- /dev/null +++ b/lib/agent/apiv1-monitoring.go @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2017-2019 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 + * + * http://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 agent + +import ( + "net/http" + + "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/aglafb" + common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git" + "github.com/gin-gonic/gin" +) + +//************************* AGL XDS Monitoring ************************* + +// getMonitoringTopo : return current AGL daemons topology using monitoring +func (s *APIService) getMonitoringTopo(c *gin.Context) { + + xdspvr, err := s._initXdsMonitoring() + if err != nil { + common.APIError(c, err.Error()) + return + } + + var res aglafb.AfbReply + if err = xdspvr.GetTopo(&res); err != nil { + common.APIError(c, err.Error()) + return + } + + if res.Request.Status != "success" { + common.APIError(c, res.Request.Info) + return + } + + c.JSON(http.StatusOK, res.Response) +} + +// startMonitoring : resquest to monitoring to start tracing +func (s *APIService) startMonitoring(c *gin.Context) { + + xdspvr, err := s._initXdsMonitoring() + if err != nil { + common.APIError(c, err.Error()) + return + } + + var cfg XdsSuperVTraceConfig + if c.BindJSON(&cfg) != nil { + common.APIError(c, "Invalid config argument") + return + } + s.Log.Debugf("Start Monitoring cfgArg %v", cfg) + + res := aglafb.NewAfbReply() + if err = xdspvr.StartTrace(cfg, res); err != nil { + common.APIError(c, err.Error()) + return + } + + if !res.Success() { + common.APIError(c, res.GetInfo()) + return + } + + c.JSON(http.StatusOK, res.Response) +} + +// stopMonitoring : resquest to monitoring to stop tracing +func (s *APIService) stopMonitoring(c *gin.Context) { + + xdspvr, err := s._initXdsMonitoring() + if err != nil { + common.APIError(c, err.Error()) + return + } + + var res aglafb.AfbReply + if err = xdspvr.StopTrace(&res); err != nil { + common.APIError(c, err.Error()) + return + } + + if res.Request.Status != "success" { + common.APIError(c, res.Request.Info) + return + } + + c.JSON(http.StatusOK, res.Response) +} + +// _initXdsMonitoring . +func (s *APIService) _initXdsMonitoring() (*XdsMonitoring, error) { + + if s.XdsMonitoring == nil { + xs := NewXdsMonitoring(s.Context) + if err := xs.Connect(); err != nil { + return nil, err + } + s.XdsMonitoring = xs + } + return s.XdsMonitoring, nil +} + +//************************* AGL Low Collector ************************* + +// XdsLowCollectorConfig Configuration structure for ALC +type XdsLowCollectorConfig struct { + Time int `json:"time"` +} + +// StartLowCollector : resquest to Start low collector +func (s *APIService) StartLowCollector(c *gin.Context) { + + alc, err := s._initXdsLowCollector() + if err != nil { + common.APIError(c, err.Error()) + return + } + + s.Log.Debugf("Init & config AGL Low Collector") + + if err = alc.Init(); err != nil { + common.APIError(c, err.Error()) + return + } + + // // Config is optional, if not set used define settings + var cfg XdsLowCollectorConfig + c.ShouldBindJSON(&cfg) + + s.Log.Debugf("Start Low Collector cfgArg %v", cfg) + + if err = alc.Start(cfg.Time); err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, "done") +} + +// StopLowCollector : resquest to Stop low collector +func (s *APIService) StopLowCollector(c *gin.Context) { + + alc, err := s._initXdsLowCollector() + if err != nil { + common.APIError(c, err.Error()) + return + } + + s.Log.Debugf("Stop AGL Low Collector") + + if err = alc.Stop(); err != nil { + common.APIError(c, err.Error()) + } + + // SEB TODO + res := "done" + c.JSON(http.StatusOK, res) +} + +// ReadLowCollector : read one data +func (s *APIService) ReadLowCollector(c *gin.Context) { + + alc, err := s._initXdsLowCollector() + if err != nil { + common.APIError(c, err.Error()) + return + } + plugin := "cpu" + s.Log.Debugf("Read data of %s plugin AGL Low Collector", plugin) + + var data interface{} + if err = alc.Read(&data); err != nil { + common.APIError(c, err.Error()) + } + + // SEB TODO + res := "done" + c.JSON(http.StatusOK, res) +} + +// ResetLowCollector : Reset Low Collector +func (s *APIService) ResetLowCollector(c *gin.Context) { + // SEB TODO + common.APIError(c, "Not implemented yet") +} + +// _initXdsLowCollector . +func (s *APIService) _initXdsLowCollector() (*XdsLowCollector, error) { + + if s.XdsLowCollector == nil { + alc := NewXdsLowCollector(s.Context) + if err := alc.Connect(); err != nil { + return nil, err + } + s.XdsLowCollector = alc + } + return s.XdsLowCollector, nil +} diff --git a/lib/agent/apiv1-supervisor.go b/lib/agent/apiv1-supervisor.go deleted file mode 100644 index 20b9549..0000000 --- a/lib/agent/apiv1-supervisor.go +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2017-2018 "IoT.bzh" - * Author Sebastien Douheret - * - * 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 - * - * http://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 agent - -import ( - "net/http" - - common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git" - "github.com/gin-gonic/gin" -) - -// getSupervisorTopo : return current AGL daemons topology using supervisor -func (s *APIService) getSupervisorTopo(c *gin.Context) { - - xdspvr, err := s._initXdsSupervisor() - if err != nil { - common.APIError(c, err.Error()) - return - } - - var res XdsSuperVReply - if err = xdspvr.GetTopo(&res); err != nil { - common.APIError(c, err.Error()) - return - } - - if res.Request.Status != "success" { - common.APIError(c, res.Request.Info) - return - } - - c.JSON(http.StatusOK, res.Response) -} - -// startSupervisor : resquest to supervisor to start tracing -func (s *APIService) startSupervisor(c *gin.Context) { - - xdspvr, err := s._initXdsSupervisor() - if err != nil { - common.APIError(c, err.Error()) - return - } - - var cfg XdsSuperVTraceConfig - if c.BindJSON(&cfg) != nil { - common.APIError(c, "Invalid config argument") - return - } - s.Log.Debugf("Start Supervisor cfgArg %v\n", cfg) - - var res XdsSuperVReply - if err = xdspvr.StartTrace(cfg, &res); err != nil { - common.APIError(c, err.Error()) - return - } - - if res.Request.Status != "success" { - common.APIError(c, res.Request.Info) - return - } - - c.JSON(http.StatusOK, res.Response) -} - -// stopSupervisor : resquest to supervisor to stop tracing -func (s *APIService) stopSupervisor(c *gin.Context) { - - xdspvr, err := s._initXdsSupervisor() - if err != nil { - common.APIError(c, err.Error()) - return - } - - var res XdsSuperVReply - if err = xdspvr.StopTrace(&res); err != nil { - common.APIError(c, err.Error()) - return - } - - if res.Request.Status != "success" { - common.APIError(c, res.Request.Info) - return - } - - c.JSON(http.StatusOK, res.Response) -} - -// _initXdsSupervisor . -func (s *APIService) _initXdsSupervisor() (*XdsSupervisor, error) { - - if s.XdsSupervisor == nil { - xs := NewXdsSupervisor(s.Context) - if err := xs.Connect(); err != nil { - return nil, err - } - s.XdsSupervisor = xs - } - return s.XdsSupervisor, nil -} diff --git a/lib/agent/apiv1.go b/lib/agent/apiv1.go index 4637bc4..ef9704c 100644 --- a/lib/agent/apiv1.go +++ b/lib/agent/apiv1.go @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2018 "IoT.bzh" + * Copyright (C) 2017-2019 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -67,9 +67,13 @@ func NewAPIV1(ctx *Context) *APIService { s.apiRouter.POST("/events/register", s.eventsRegister) s.apiRouter.POST("/events/unregister", s.eventsUnRegister) - s.apiRouter.GET("/supervisor/topo", s.getSupervisorTopo) - s.apiRouter.POST("/supervisor/trace/start", s.startSupervisor) - s.apiRouter.POST("/supervisor/trace/stop", s.stopSupervisor) + s.apiRouter.GET("/monitoring/topo", s.getMonitoringTopo) + s.apiRouter.POST("/monitoring/trace/start", s.startMonitoring) + s.apiRouter.POST("/monitoring/trace/stop", s.stopMonitoring) + s.apiRouter.POST("/monitoring/alc/start", s.StartLowCollector) + s.apiRouter.POST("/monitoring/alc/stop", s.StopLowCollector) + s.apiRouter.GET("/monitoring/alc/read", s.ReadLowCollector) + s.apiRouter.POST("/monitoring/alc/reset", s.ResetLowCollector) return s } diff --git a/lib/agent/xds-low-collector.go b/lib/agent/xds-low-collector.go new file mode 100644 index 0000000..fdf696d --- /dev/null +++ b/lib/agent/xds-low-collector.go @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2019 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 + * + * http://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 agent + +import ( + "fmt" + "io" + "strings" + "time" + + "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/aglafb" + common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git" + uuid "github.com/satori/go.uuid" +) + +// XdsLowCollector . +type XdsLowCollector struct { + *Context + ID string + BaseURL string + ConnRetry int + Connected bool + Disabled bool + DefaultPlugins []string + DefaultCollectTime int + // Private fields + client *common.HTTPClient + logOut io.Writer + cbOnConnect OnConnectedXdsAlcCB +} + +// OnConnectedXdsAlcCB connect callback +type OnConnectedXdsAlcCB func(svr *XdsLowCollector) error + +// NewXdsLowCollector creates an instance of XdsLowCollector +func NewXdsLowCollector(ctx *Context) *XdsLowCollector { + return &XdsLowCollector{ + Context: ctx, + ID: "XdsAlc-" + uuid.NewV1().String(), + BaseURL: ctx.Config.FileConf.ProfileConf.XDSLowCollector.URL, + ConnRetry: ctx.Config.FileConf.ProfileConf.XDSLowCollector.ConnRetry, + Connected: false, + Disabled: false, + logOut: ctx.Log.Out, + DefaultPlugins: []string{ + "cpu", + "memory", + // SEB "processes", + //"cpufreq", + //"thermal", + //"systemd_journal", + // SEB "systemd_file", + }, + DefaultCollectTime: 5, + } +} + +// Connect Establish HTTP connection with XDS Low Collector Dameon +func (xs *XdsLowCollector) Connect() error { + var err error + var retry int + + xs.Disabled = false + xs.Connected = false + + err = nil + for retry = xs.ConnRetry; retry > 0; retry-- { + if err = xs._CreateConnectHTTP(); err == nil { + break + } + if retry == xs.ConnRetry { + // Notify only on the first conn error + // doing that avoid 2 notifs (conn false; conn true) on startup + xs._NotifyState() + } + xs.Log.Infof("Establishing connection to XDS Low Collector daemon (retry %d/%d)", retry, xs.ConnRetry) + time.Sleep(time.Second) + } + if retry == 0 { + // FIXME: re-use _Reconnect to wait longer in background + return fmt.Errorf("Connection to XDS Low Collector daemon failure") + } + if err != nil { + return err + } + + // Check HTTP connection and establish WS connection + err = xs._Connect(false) + + return err +} + +// ConnectOn Register a callback on events reception +func (xs *XdsLowCollector) ConnectOn(f OnConnectedXdsAlcCB) error { + xs.cbOnConnect = f + return nil +} + +// GetVersion Send Get request to retrieve XDS Low Collector version +func (xs *XdsLowCollector) GetVersion(res interface{}) error { + // FIXME add suffix URLSuffix in common HTTP client lib instead of _BuildURL + return xs.client.Get(xs._BuildURL("/version"), &res) +} + +// Init Initialize collector plugins +func (xs *XdsLowCollector) Init() error { + var err error + + // Directly send config in order to init and config plugins + + type alcCfgPluginT struct { + Plugin string `json:"plugin"` + Config string `json:"config"` + } + + cfg := []alcCfgPluginT{} + + for _, p := range xs.DefaultPlugins { + cfg = append(cfg, alcCfgPluginT{ + Plugin: p, + Config: "default", + }) + } + + res := aglafb.NewAfbReply() + xs.Log.Debugf("Low Collector /config %v", cfg) + err = xs.client.Post(xs._BuildURL("/config"), cfg, res) + + if err == nil && !res.Success() { + err = res.GetError() + } + + return err +} + +// Start data collection +func (xs *XdsLowCollector) Start(time int) error { + var err error + + // TODO - SEB : support start one or multiple plugins + + if time == 0 { + time = xs.DefaultCollectTime + } + + type alcStartT struct { + Plugin string `json:"plugin"` + Time int `json:"time"` + } + + // TODO SEB : allow to start only 1 plugin + allInOne := true + if allInOne { + + cfg := []alcStartT{} + for _, p := range xs.DefaultPlugins { + cfg = append(cfg, alcStartT{Plugin: p, Time: time}) + } + + res := aglafb.NewAfbReply() + xs.Log.Debugf("Low Collector /start %v", cfg) + err = xs.client.Post(xs._BuildURL("/start"), cfg, res) + + if err == nil && !res.Success() { + err = res.GetError() + } + } else { + for _, p := range xs.DefaultPlugins { + cfg := alcStartT{Plugin: p, Time: time} + + res := aglafb.NewAfbReply() + xs.Log.Debugf("Low Collector /start %v", cfg) + err = xs.client.Post(xs._BuildURL("/start"), cfg, res) + if err != nil { + return err + } + if !res.Success() { + return res.GetError() + } + } + } + + return err +} + +// Stop data collection +func (xs *XdsLowCollector) Stop() error { + + // TODO - SEB : support start one or multiple plugins + + type alcStopT struct { + Plugin []string `json:"plugin"` + } + + cfg := alcStopT{} + for _, p := range xs.DefaultPlugins { + cfg.Plugin = append(cfg.Plugin, p) + } + + res := aglafb.NewAfbReply() + xs.Log.Debugf("Low Collector /stop %v", cfg) + err := xs.client.Post(xs._BuildURL("/stop"), cfg, res) + + if err == nil && !res.Success() { + err = res.GetError() + } + + return err +} + +// Read a single data of a specific plugin +func (xs *XdsLowCollector) Read(data interface{}) error { + return fmt.Errorf("No implemented") +} + +/*** +** Private functions +***/ + +// _BuildURL . +func (xs *XdsLowCollector) _BuildURL(url string) string { + return url + "?token=HELLO&uuid=magic" +} + +// Create HTTP client +func (xs *XdsLowCollector) _CreateConnectHTTP() error { + var err error + // FIXME SEB - Client key not in header but in cookie + // temporary workaround: used _BuildURL to append uuid=magic in URL + // map[Set-Cookie:[x-afb-uuid-5678=2b185cc3-276b-4097-91fa-d607eaf937e6; Path=/api; Max-Age=32000000; ... + //port := strings.Split(xs.BaseURL, ":")[2] + //"x-afb-uuid-" + port + + xs.client, err = common.HTTPNewClient(xs.BaseURL, + common.HTTPClientConfig{ + //HeaderClientKeyName: "Xds-Sid", + HeaderAPIKeyName: "token", + Apikey: "HELLO", + URLPrefix: "/api/alc", + CsrfDisable: true, + LogOut: xs.logOut, + LogPrefix: "XDSALC: ", + LogLevel: common.HTTPLogLevelWarning, + }) + + xs.client.SetLogLevel(xs.Log.Level.String()) + + if err != nil { + msg := ": " + err.Error() + if strings.Contains(err.Error(), "connection refused") { + msg = fmt.Sprintf("(url: %s)", xs.BaseURL) + } + return fmt.Errorf("ERROR: cannot connect to XDS Low Collector %s", msg) + } + if xs.client == nil { + return fmt.Errorf("ERROR: cannot connect to XDS Low Collector (null client)") + } + + return nil +} + +// _Connect Established HTTP and WS connection +func (xs *XdsLowCollector) _Connect(reConn bool) error { + var res interface{} + if err := xs.client.Get(xs._BuildURL("/ping"), &res); err != nil { + + // SEB FIXME tempo Hack + time.Sleep(time.Microsecond * 300) + if err := xs.client.Get(xs._BuildURL("/ping"), &res); err != nil { + // SEB Hack tempo + // xs.Connected = false + // if !reConn { + // xs._NotifyState() + // } + // return err + } + } + + xs.Connected = true + + // Call OnConnect callback + if xs.cbOnConnect != nil { + xs.cbOnConnect(xs) + } + + xs._NotifyState() + return nil +} + +// _NotifyState Send event to notify changes +func (xs *XdsLowCollector) _NotifyState() { + + /* TODO + evSts := xaapiv1.ServerCfg{ + ID: xs.ID, + URL: xs.BaseURL, + APIURL: xs.APIURL, + PartialURL: xs.PartialURL, + ConnRetry: xs.ConnRetry, + Connected: xs.Connected, + } + if err := xs.events.Emit(xaapiv1.EVTServerConfig, evSts, ""); err != nil { + xs.Log.Warningf("Cannot notify XdsServer state change: %v", err) + } + */ +} diff --git a/lib/agent/xdsmonitoring.go b/lib/agent/xdsmonitoring.go new file mode 100644 index 0000000..42e5324 --- /dev/null +++ b/lib/agent/xdsmonitoring.go @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2017-2019 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 + * + * http://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 agent + +import ( + "fmt" + "io" + "strings" + "time" + + common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git" + uuid "github.com/satori/go.uuid" +) + +// XdsMonitoring . +type XdsMonitoring struct { + *Context + ID string + BaseURL string + ConnRetry int + Connected bool + Disabled bool + + // Private fields + client *common.HTTPClient + logOut io.Writer + cbOnConnect OnConnectedXdsSupervCB +} + +// XdsSuperVTraceConfig +type XdsSuperVTraceConfig struct { + Pid int `json:"pid"` + Pids []int `json:"pids"` + WsName string `json:"ws"` +} + +// OnConnectedXdsSupervCB connect callback +type OnConnectedXdsSupervCB func(svr *XdsMonitoring) error + +// NewXdsMonitoring creates an instance of XdsMonitoring +func NewXdsMonitoring(ctx *Context) *XdsMonitoring { + return &XdsMonitoring{ + Context: ctx, + ID: "XdsMonitoring-" + uuid.NewV1().String(), + BaseURL: ctx.Config.FileConf.ProfileConf.XDSMonitoring.URL, + ConnRetry: ctx.Config.FileConf.ProfileConf.XDSMonitoring.ConnRetry, + Connected: false, + Disabled: false, + + logOut: ctx.Log.Out, + } +} + +// Connect Establish HTTP connection with XDS Monitoring Dameon +func (xs *XdsMonitoring) Connect() error { + var err error + var retry int + + xs.Disabled = false + xs.Connected = false + + err = nil + for retry = xs.ConnRetry; retry > 0; retry-- { + if err = xs._CreateConnectHTTP(); err == nil { + break + } + if retry == xs.ConnRetry { + // Notify only on the first conn error + // doing that avoid 2 notifs (conn false; conn true) on startup + xs._NotifyState() + } + xs.Log.Infof("Establishing connection to XDS Monitoring daemon (retry %d/%d)", retry, xs.ConnRetry) + time.Sleep(time.Second) + } + if retry == 0 { + // FIXME: re-use _Reconnect to wait longer in background + return fmt.Errorf("Connection to XDS Monitoring daemon failure") + } + if err != nil { + return err + } + + // Check HTTP connection and establish WS connection + err = xs._Connect(false) + + return err +} + +// ConnectOn Register a callback on events reception +func (xs *XdsMonitoring) ConnectOn(f OnConnectedXdsSupervCB) error { + xs.cbOnConnect = f + return nil +} + +// GetVersion Send Get request to retrieve XDS Monitoring version +func (xs *XdsMonitoring) GetVersion(res interface{}) error { + // FIXME add suffix URLSuffix in common HTTP client lib instead of _BuildURL + return xs.client.Get(xs._BuildURL("/version"), &res) +} + +// GetTopo Send Get request to retrieve Services/Daemons topology +func (xs *XdsMonitoring) GetTopo(res interface{}) error { + return xs.client.Get(xs._BuildURL("/list"), &res) +} + +// StartTrace Send Monitoring config and start tracing +func (xs *XdsMonitoring) StartTrace(cfg XdsSuperVTraceConfig, res interface{}) error { + return xs.client.Post(xs._BuildURL("/trace/start"), cfg, &res) +} + +// StopTrace Send Monitoring stop tracing +func (xs *XdsMonitoring) StopTrace(res interface{}) error { + var cfg interface{} + return xs.client.Post(xs._BuildURL("/trace/stop"), cfg, res) +} + +/*** +** Private functions +***/ + +// _BuildURL . +func (xs *XdsMonitoring) _BuildURL(url string) string { + return url + "?token=HELLO&uuid=magic" +} + +// Create HTTP client +func (xs *XdsMonitoring) _CreateConnectHTTP() error { + var err error + // FIXME SEB - Client key not in header but in cookie + // temporary workaround: used _BuildURL to append uuid=magic in URL + // map[Set-Cookie:[x-afb-uuid-5678=2b185cc3-276b-4097-91fa-d607eaf937e6; Path=/api; Max-Age=32000000; ... + //port := strings.Split(xs.BaseURL, ":")[2] + //"x-afb-uuid-" + port + + xs.client, err = common.HTTPNewClient(xs.BaseURL, + common.HTTPClientConfig{ + //HeaderClientKeyName: "Xds-Sid", + HeaderAPIKeyName: "token", + Apikey: "HELLO", + URLPrefix: "/api/xds", + CsrfDisable: true, + LogOut: xs.logOut, + LogPrefix: "XDSSUPERV: ", + LogLevel: common.HTTPLogLevelWarning, + }) + + xs.client.SetLogLevel(xs.Log.Level.String()) + + if err != nil { + msg := ": " + err.Error() + if strings.Contains(err.Error(), "connection refused") { + msg = fmt.Sprintf("(url: %s)", xs.BaseURL) + } + return fmt.Errorf("ERROR: cannot connect to XDS Monitoring %s", msg) + } + if xs.client == nil { + return fmt.Errorf("ERROR: cannot connect to XDS Monitoring (null client)") + } + + return nil +} + +// _Connect Established HTTP and WS connection +func (xs *XdsMonitoring) _Connect(reConn bool) error { + + var res interface{} + if err := xs.client.Get(xs._BuildURL("/ping"), &res); err != nil { + xs.Connected = false + if !reConn { + xs._NotifyState() + } + return err + } + + xs.Connected = true + + // Call OnConnect callback + if xs.cbOnConnect != nil { + xs.cbOnConnect(xs) + } + + xs._NotifyState() + return nil +} + +// _NotifyState Send event to notify changes +func (xs *XdsMonitoring) _NotifyState() { + + /* TODO + evSts := xaapiv1.ServerCfg{ + ID: xs.ID, + URL: xs.BaseURL, + APIURL: xs.APIURL, + PartialURL: xs.PartialURL, + ConnRetry: xs.ConnRetry, + Connected: xs.Connected, + } + if err := xs.events.Emit(xaapiv1.EVTServerConfig, evSts, ""); err != nil { + xs.Log.Warningf("Cannot notify XdsServer state change: %v", err) + } + */ +} diff --git a/lib/agent/xdssupervior.go b/lib/agent/xdssupervior.go deleted file mode 100644 index 48b3f90..0000000 --- a/lib/agent/xdssupervior.go +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2017-2018 "IoT.bzh" - * Author Sebastien Douheret - * - * 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 - * - * http://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 agent - -import ( - "fmt" - "io" - "strings" - "time" - - common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git" - uuid "github.com/satori/go.uuid" -) - -// XdsSupervisor . -type XdsSupervisor struct { - *Context - ID string - BaseURL string - ConnRetry int - Connected bool - Disabled bool - - // Private fields - client *common.HTTPClient - logOut io.Writer - cbOnConnect OnConnectedXdsSupervCB -} - -// XdsSuperVRequest Resquest field of a reply -type XdsSuperVRequest struct { - Status string `json:"status"` - Info string `json:"info"` -} - -// XdsSuperVReply Reply structure of XDS Supervision Daemon -type XdsSuperVReply struct { - JType string `json:"jtype"` - Request XdsSuperVRequest `json:"request"` - Response interface{} `json:"response"` -} - -// XdsSuperVTraceConfig -type XdsSuperVTraceConfig struct { - Pid int `json:"pid"` - Pids []int `json:"pids"` - WsName string `json:"ws"` -} - -// OnConnectedXdsSupervCB connect callback -type OnConnectedXdsSupervCB func(svr *XdsSupervisor) error - -// NewXdsSupervisor creates an instance of XdsSupervisor -func NewXdsSupervisor(ctx *Context) *XdsSupervisor { - return &XdsSupervisor{ - Context: ctx, - ID: "XdsSupervisor-" + uuid.NewV1().String(), - BaseURL: ctx.Config.FileConf.ProfileConf.XDSBinder.URL, - ConnRetry: ctx.Config.FileConf.ProfileConf.XDSBinder.ConnRetry, - Connected: false, - Disabled: false, - - logOut: ctx.Log.Out, - } -} - -// Connect Establish HTTP connection with XDS Supervisor Dameon -func (xs *XdsSupervisor) Connect() error { - var err error - var retry int - - xs.Disabled = false - xs.Connected = false - - err = nil - for retry = xs.ConnRetry; retry > 0; retry-- { - if err = xs._CreateConnectHTTP(); err == nil { - break - } - if retry == xs.ConnRetry { - // Notify only on the first conn error - // doing that avoid 2 notifs (conn false; conn true) on startup - xs._NotifyState() - } - xs.Log.Infof("Establishing connection to XDS Supervisor daemon (retry %d/%d)", retry, xs.ConnRetry) - time.Sleep(time.Second) - } - if retry == 0 { - // FIXME: re-use _Reconnect to wait longer in background - return fmt.Errorf("Connection to XDS Supervisor daemon failure") - } - if err != nil { - return err - } - - // Check HTTP connection and establish WS connection - err = xs._Connect(false) - - return err -} - -// ConnectOn Register a callback on events reception -func (xs *XdsSupervisor) ConnectOn(f OnConnectedXdsSupervCB) error { - xs.cbOnConnect = f - return nil -} - -// GetVersion Send Get request to retrieve XDS Supervision version -func (xs *XdsSupervisor) GetVersion(res interface{}) error { - // FIXME add suffix URLSuffix in common HTTP client lib instead of _BuildURL - return xs.client.Get(xs._BuildURL("/version"), &res) -} - -// GetTopo Send Get request to retrieve Services/Daemons topology -func (xs *XdsSupervisor) GetTopo(res interface{}) error { - return xs.client.Get(xs._BuildURL("/list"), &res) -} - -// StartTrace Send Supervisor config and start tracing -func (xs *XdsSupervisor) StartTrace(cfg XdsSuperVTraceConfig, res interface{}) error { - return xs.client.Post(xs._BuildURL("/trace/start"), cfg, &res) -} - -// StopTrace Send Supervisor stop tracing -func (xs *XdsSupervisor) StopTrace(res interface{}) error { - var cfg interface{} - return xs.client.Post(xs._BuildURL("/trace/stop"), cfg, res) -} - -/*** -** Private functions -***/ - -// _BuildURL . -func (xs *XdsSupervisor) _BuildURL(url string) string { - return url + "?token=HELLO&uuid=magic" -} - -// Create HTTP client -func (xs *XdsSupervisor) _CreateConnectHTTP() error { - var err error - // FIXME SEB - Client key not in header but in cookie - // temporary workaround: used _BuildURL to append uuid=magic in URL - // map[Set-Cookie:[x-afb-uuid-5678=2b185cc3-276b-4097-91fa-d607eaf937e6; Path=/api; Max-Age=32000000; ... - //port := strings.Split(xs.BaseURL, ":")[2] - //"x-afb-uuid-" + port - - xs.client, err = common.HTTPNewClient(xs.BaseURL, - common.HTTPClientConfig{ - //HeaderClientKeyName: "Xds-Sid", - HeaderAPIKeyName: "token", - Apikey: "HELLO", - URLPrefix: "/api/xds", - CsrfDisable: true, - LogOut: xs.logOut, - LogPrefix: "XDSSUPERV: ", - LogLevel: common.HTTPLogLevelWarning, - }) - - xs.client.SetLogLevel(xs.Log.Level.String()) - - if err != nil { - msg := ": " + err.Error() - if strings.Contains(err.Error(), "connection refused") { - msg = fmt.Sprintf("(url: %s)", xs.BaseURL) - } - return fmt.Errorf("ERROR: cannot connect to XDS Supervisor %s", msg) - } - if xs.client == nil { - return fmt.Errorf("ERROR: cannot connect to XDS Supervisor (null client)") - } - - return nil -} - -// _Connect Established HTTP and WS connection -func (xs *XdsSupervisor) _Connect(reConn bool) error { - - var res interface{} - if err := xs.client.Get(xs._BuildURL("/ping"), &res); err != nil { - xs.Connected = false - if !reConn { - xs._NotifyState() - } - return err - } - - xs.Connected = true - - // Call OnConnect callback - if xs.cbOnConnect != nil { - xs.cbOnConnect(xs) - } - - xs._NotifyState() - return nil -} - -// _NotifyState Send event to notify changes -func (xs *XdsSupervisor) _NotifyState() { - - /* TODO - evSts := xaapiv1.ServerCfg{ - ID: xs.ID, - URL: xs.BaseURL, - APIURL: xs.APIURL, - PartialURL: xs.PartialURL, - ConnRetry: xs.ConnRetry, - Connected: xs.Connected, - } - if err := xs.events.Emit(xaapiv1.EVTServerConfig, evSts, ""); err != nil { - xs.Log.Warningf("Cannot notify XdsServer state change: %v", err) - } - */ -} diff --git a/lib/aglafb/afb.go b/lib/aglafb/afb.go new file mode 100644 index 0000000..85f6686 --- /dev/null +++ b/lib/aglafb/afb.go @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 "IoT.bzh" + * Author Sebastien Douheret + * + * 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 + * + * http://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 aglafb + +import "fmt" + +// AfbRequest Resquest field of a reply +type AfbRequest struct { + Status string `json:"status"` + Info string `json:"info"` +} + +// AfbReply Reply structure of XDS Monitoring Daemon +type AfbReply struct { + JType string `json:"jtype"` + Request AfbRequest `json:"request"` + Response interface{} `json:"response"` +} + +func NewAfbReply() *AfbReply { + return &AfbReply{} +} + +func (r *AfbReply) Success() bool { + return r.Request.Status == "success" +} + +func (r *AfbReply) Failure() bool { + return !r.Success() +} + +func (r *AfbReply) GetError() error { + return fmt.Errorf(r.Request.Info) +} + +func (r *AfbReply) GetInfo() string { + return r.Request.Info +} diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go index 93c8f4b..6a5e750 100644 --- a/lib/xdsconfig/config.go +++ b/lib/xdsconfig/config.go @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2018 "IoT.bzh" + * Copyright (C) 2017-2019 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -92,10 +92,14 @@ func Init(ctx *cli.Context, log *logrus.Logger) (*Config, error) { Home: defaultSTHomeDir, }, ProfileConf: ProfileConfT{ - XDSBinder: XDSBinderConf{ + XDSMonitoring: XDSMonitoringConf{ URL: "http://localhost:8810", ConnRetry: 10, }, + XDSLowCollector: XDSLowCollectorConf{ + URL: "http://localhost:8820", + ConnRetry: 10, + }, }, }, Log: log, diff --git a/lib/xdsconfig/configfile.go b/lib/xdsconfig/configfile.go index 7ddb010..85e26bc 100644 --- a/lib/xdsconfig/configfile.go +++ b/lib/xdsconfig/configfile.go @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2018 "IoT.bzh" + * Copyright (C) 2017-2019 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,13 +43,19 @@ type XDSServerConf struct { APIPartialURL string `json:"-"` } -type XDSBinderConf struct { +type XDSMonitoringConf struct { + URL string `json:"url"` + ConnRetry int `json:"connRetry"` +} + +type XDSLowCollectorConf struct { URL string `json:"url"` ConnRetry int `json:"connRetry"` } type ProfileConfT struct { - XDSBinder XDSBinderConf `json:"xdsBinder"` + XDSMonitoring XDSMonitoringConf `json:"xdsMonitoring"` + XDSLowCollector XDSLowCollectorConf `json:"xdsLowCollector"` } type FileConfig struct { diff --git a/webapp/src/app/@core-xds/services/@core-xds-services.module.ts b/webapp/src/app/@core-xds/services/@core-xds-services.module.ts index 6a4eb3c..b3606ec 100644 --- a/webapp/src/app/@core-xds/services/@core-xds-services.module.ts +++ b/webapp/src/app/@core-xds/services/@core-xds-services.module.ts @@ -23,7 +23,7 @@ import { AlertService } from './alert.service'; import { ConfigService } from './config.service'; import { ProjectService } from './project.service'; import { SdkService } from './sdk.service'; -import { SupervisionService } from './supervision.service'; +import { MonitoringService } from './monitoring.service'; import { TargetService } from './target.service'; import { UserService } from './users.service'; import { XDSConfigService } from './xds-config.service'; @@ -34,7 +34,7 @@ const SERVICES = [ ConfigService, ProjectService, SdkService, - SupervisionService, + MonitoringService, TargetService, UserService, XDSConfigService, diff --git a/webapp/src/app/@core-xds/services/monitoring.service.ts b/webapp/src/app/@core-xds/services/monitoring.service.ts new file mode 100644 index 0000000..a606909 --- /dev/null +++ b/webapp/src/app/@core-xds/services/monitoring.service.ts @@ -0,0 +1,106 @@ +/** +* @license +* Copyright (C) 2018-2019 "IoT.bzh" +* Author Sebastien Douheret +* +* 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 +* +* http://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. +*/ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { XDSAgentService } from './xdsagent.service'; +import { map } from 'rxjs/operators'; + +export interface AglTopology { + name: string; + pid: number; + disabled: boolean; + isClient: boolean; + isServer: boolean; + ws_clients: string[]; + ws_servers: string[]; + apis: any; +} + +export interface AglLowCollectorConfig { + time?: number; +} + +@Injectable() +export class MonitoringService { + + constructor(private xdsSvr: XDSAgentService) { + /* + this.xdsSvr.XdsConfig$.subscribe(cfg => { + if (!cfg || cfg.servers.length < 1) { + return; + } + }); + */ + } + + getTopo(): Observable { + return this.xdsSvr.getTopoMonitoring().pipe( + map((tp: AglTopology[]) => { + // FIXME - move filter on backend side + const ignored: string[] = [ + 'agl-low-collector', + 'harvester', + ]; + + tp.forEach(el => { + el.disabled = false; + ignored.forEach(iel => { + if (el.name.indexOf(iel) !== -1) { + el.disabled = true; + } + }); + + // replace unix:/run/xxx/ws by nothing + const wsc: string[] = []; + el.ws_clients.forEach(s => { + s = s.replace('unix:/run/platform/apis/ws/', ''); + s = s.replace('unix:/run/user/1001/apis/ws/', ''); + s = s.replace('unix:/run/user/0/apis/ws/', ''); + wsc.push(s); + }); + el.ws_clients = wsc; + // replace sd: by nothing + const wss: string[] = []; + el.ws_servers.forEach(s => { + wss.push(s.replace('sd:', '')); + }); + el.ws_servers = wss; + }); + return tp; + }) + ); + } + + startTrace(cfg: any): Observable { + return this.xdsSvr.startTraceMonitoring(cfg); + } + + stopTrace(cfg: any): Observable { + return this.xdsSvr.stopTraceMonitoring(cfg); + } + + startLowCollector(cfg: AglLowCollectorConfig): Observable { + return this.xdsSvr.startLowCollector(cfg); + } + + stopLowCollector(): Observable { + return this.xdsSvr.stopLowCollector(); + } +} diff --git a/webapp/src/app/@core-xds/services/supervision.service.ts b/webapp/src/app/@core-xds/services/supervision.service.ts deleted file mode 100644 index 4a9f578..0000000 --- a/webapp/src/app/@core-xds/services/supervision.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** -* @license -* Copyright (C) 2018 "IoT.bzh" -* Author Sebastien Douheret -* -* 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 -* -* http://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. -*/ - -import { Injectable, SecurityContext, isDevMode } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; - -import { XDSAgentService } from '../services/xdsagent.service'; - -export interface AglTopology { - name: string; - pid: number; - isClient: boolean; - isServer: boolean; - ws_clients: string[]; - ws_servers: string[]; - apis: any; -} - -@Injectable() -export class SupervisionService { - - private curServerID; - - constructor(private xdsSvr: XDSAgentService) { - /* - this.xdsSvr.XdsConfig$.subscribe(cfg => { - if (!cfg || cfg.servers.length < 1) { - return; - } - }); - */ - } - - getTopo(): Observable { - return this.xdsSvr.getTopoSupervisor(); - } - - startTrace(cfg: any): Observable { - return this.xdsSvr.startTraceSupervisor(cfg); - } - - stopTrace(cfg: any): Observable { - return this.xdsSvr.stopTraceSupervisor(cfg); - } - -} diff --git a/webapp/src/app/@core-xds/services/xdsagent.service.ts b/webapp/src/app/@core-xds/services/xdsagent.service.ts index 002c84b..35abe46 100644 --- a/webapp/src/app/@core-xds/services/xdsagent.service.ts +++ b/webapp/src/app/@core-xds/services/xdsagent.service.ts @@ -1,6 +1,6 @@ /** * @license -* Copyright (C) 2017-2018 "IoT.bzh" +* Copyright (C) 2017-2019 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -666,18 +666,33 @@ export class XDSAgentService { } /*** - ** Supervision + ** Monitoring ***/ - getTopoSupervisor(): Observable { - return this._get('/supervisor/topo'); + getTopoMonitoring(): Observable { + return this._get('/monitoring/topo'); } - startTraceSupervisor(cfg: any): Observable { - return this._post('/supervisor/trace/start', cfg); + startTraceMonitoring(cfg: any): Observable { + return this._post('/monitoring/trace/start', cfg); } - stopTraceSupervisor(cfg: any): Observable { - return this._post('/supervisor/trace/stop', cfg); + stopTraceMonitoring(cfg: any): Observable { + return this._post('/monitoring/trace/stop', cfg); + } + + /*** + ** AGL Low Collector + ***/ + startLowCollector(cfg: any): Observable { + return this._post('/monitoring/alc/start', cfg); + } + + stopLowCollector(): Observable { + return this._post('/monitoring/alc/stop', {}); + } + + resetLowCollector(): Observable { + return this._post('/monitoring/alc/reset', {}); } /** @@ -692,7 +707,7 @@ export class XDSAgentService { error => { this.alert.error('ERROR while registering to all events: ' + error); }, - ); + ); } private _getServer(ID: string): IXDServerCfg { diff --git a/webapp/src/app/pages/config/config-global/config-global.component.html b/webapp/src/app/pages/config/config-global/config-global.component.html index c3bc8b4..3ae77d9 100644 --- a/webapp/src/app/pages/config/config-global/config-global.component.html +++ b/webapp/src/app/pages/config/config-global/config-global.component.html @@ -26,7 +26,7 @@
- Supervision Configuration + Monitoring Configuration
diff --git a/webapp/src/app/pages/monitoring/monitoring-config.component.html b/webapp/src/app/pages/monitoring/monitoring-config.component.html new file mode 100644 index 0000000..ed76dd0 --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring-config.component.html @@ -0,0 +1,77 @@ +
+
+

Configuration

+
+
+ +
+ + + + + +
+
+
+ +
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + No Items! + loading... + + loading... + +
NamePidWS ClientsWS ServersMonitor
+ {{tp.topo[col]}} + + + + + + DISABLED + + +
+
+ +
+

AGL Bindings Topology

+
+
+ + +
diff --git a/webapp/src/app/pages/monitoring/monitoring-config.component.scss b/webapp/src/app/pages/monitoring/monitoring-config.component.scss new file mode 100644 index 0000000..614f79d --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring-config.component.scss @@ -0,0 +1,153 @@ +/* FIXME: not working due to workaround (ViewEncapsulation.None) for svg +@import '../../@theme/styles/themes'; +@import '~@nebular/theme/components/card/card.component.theme'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-card-body { + display: flex; + align-items: center; + } + .action-groups-header { + flex-basis: 20%; + color: nb-theme(card-header-fg-heading); + font-family: nb-theme(card-header-font-family); + font-size: nb-theme(card-header-font-size); + font-weight: nb-theme(card-header-font-weight); + } + .nb-actions { + flex-basis: 80%; + } + .right { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + order: 1; + flex-direction: row-reverse; + } + nb-actions > nb-action { + padding: 0; + } + nb-action { + i { + color: nb-theme(color-fg); + font-size: 1.5rem; + margin-right: 1rem; + } + span { + font-family: nb-theme(font-secondary); + font-weight: nb-theme(font-weight-bold); + color: nb-theme(color-fg-heading); + text-transform: uppercase; + } + button { + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } + } + } + @include media-breakpoint-down(md) { + nb-actions nb-action { + padding: 0 0.75rem; + } + } + @include media-breakpoint-down(sm) { + nb-card-body { + padding: 1rem; + } + nb-action { + font-size: 0.75rem; + i { + font-size: 2rem; + margin-right: 0.5rem; + } + } + } + @include media-breakpoint-down(is) { + nb-action i { + font-size: 1.75rem; + margin: 0; + } + span { + display: none; + } + } +} +*/ + +button#refresh-topo { + color: #a4abb3; + font-size: 1.5rem; + margin-right: 1rem; + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } +} + +table { + table-layout: fixed; +} + +td { + word-wrap:break-word; +} + +button#start-trace { + margin-top: 10px; + margin-left: 10px; +} + +button#stop-trace { + margin-top: 10px; + margin-left: 10px; +} + +button#show-graph { + margin-top: 10px; + margin-left: 10px; +} + +#ws-client { + fill: green; +} + +svg#graph { + .link { + fill: none; + stroke: #666; + stroke-width: 1.5px; + } + .link.ws-client { + stroke: green; + } + .link.not-connected { + stroke-dasharray: 0, 2 1; + } + circle { + fill: #ccc; + stroke: #333; + stroke-width: 1.5px; + } + text { + font: 1rem sans-serif; + pointer-events: none; + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; + } +} diff --git a/webapp/src/app/pages/monitoring/monitoring-config.component.ts b/webapp/src/app/pages/monitoring/monitoring-config.component.ts new file mode 100644 index 0000000..2a5a84b --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring-config.component.ts @@ -0,0 +1,362 @@ +/** +* @license +* Copyright (C) 2017-2019 "IoT.bzh" +* Author Sebastien Douheret +* +* 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 +* +* http://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. +*/ + +import { Component, OnInit, AfterViewInit, ViewEncapsulation, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import * as d3 from 'd3'; +import { Router } from '@angular/router'; + +import { MonitoringService, AglTopology } from '../../@core-xds/services/monitoring.service'; +import { AlertService } from '../../@core-xds/services/alert.service'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Subscription } from 'rxjs/Subscription'; + +interface WsCheckbox { + topo: AglTopology; + value: boolean; + tooltip: string; +} + +@Component({ + selector: 'xds-monitoring', + styleUrls: ['./monitoring-config.component.scss'], + templateUrl: './monitoring-config.component.html', + encapsulation: ViewEncapsulation.None, // workaround about https://github.com/angular/angular/issues/7845 +}) +export class MonitoringConfigComponent implements OnInit, AfterViewInit { + + aglTopoInit = new BehaviorSubject(false); + // FIXME: use Map instead of array and use '| keyvalue' for ngfor loop (but angular > 6.1 requested) + // daemonCheckboxes: Map = new Map(); + daemonCheckboxes: WsCheckbox[] = []; + starting = false; + stopping = false; + + private graph: any; + private svg: any; + private links = []; + private _aglTopoSub: Subscription; + + constructor(@Inject(DOCUMENT) private document: Document, + private router: Router, + private monitoringSvr: MonitoringService, + private alert: AlertService, + ) { + + } + + ngOnInit() { + } + + ngAfterViewInit() { + this.getAGLTopo(); + this.aglTopoInit.next(true); + } + + getAGLTopo() { + if (this._aglTopoSub !== undefined) { + this._aglTopoSub.unsubscribe(); + } + + this._aglTopoSub = this.monitoringSvr.getTopo().subscribe(topo => { + this.graphAGLBindings(topo); + this.createCheckboxes(topo); + }); + } + + onStartTrace() { + this.starting = true; + + const dmArr = []; + this.daemonCheckboxes.forEach(dm => dm.value && dmArr.push(dm.topo.pid)); + + this.monitoringSvr.startTrace({ pids: dmArr }).subscribe(res => { + // console.log('Trace Started: res', res); + + this.monitoringSvr.startLowCollector(null).subscribe((/*res*/) => { + // console.log('Low Collector Started: res', res); + this.alert.info('Monitoring successfully started'); + this.starting = false; + + }, err => { + this.starting = false; + this.alert.error(err); + }); + + }, err => { + this.starting = false; + this.alert.error(err); + }); + } + + onStopTrace() { + this.stopping = true; + this.monitoringSvr.stopTrace({}).subscribe(res => { + // console.log('Trace Stopped: res', res); + + this.monitoringSvr.stopLowCollector().subscribe((/*res*/) => { + // console.log('Low Collector Stopped: res', res); + this.alert.info('Monitoring successfully started'); + this.stopping = false; + + }, err => { + this.stopping = false; + this.alert.error(err); + }); + + }, err => { + this.stopping = false; + this.alert.error(err); + }); + } + + showGraph() { + this.router.navigate([`/pages/monitoring/graph`]); + } + + isStartBtnDisable(): boolean { + return this.starting; + } + + isStopBtnDisable(): boolean { + return this.stopping; + } + + isDaemonDisabled(name: string): boolean { + let sts = false; + // FIXME - better to use map + // with Map + // if (this.daemonCheckboxes.has(name)) { + // sts = this.daemonCheckboxes[name].value; + // } + this.daemonCheckboxes.forEach(e => { + if (e.topo.name === name) { + sts = true; + } + }); + return sts; + } + + private createCheckboxes(topo: AglTopology[]) { + + // let newDaemonChB: Map = new Map(); + const newDaemonChB: WsCheckbox[] = []; + let prevVal = false; + this.daemonCheckboxes.forEach(e => { + if (e.topo.name === name) { + prevVal = e.value; + } + }); + topo.forEach(elem => { + // with Map + // newDaemonChB.set(elem.name, { + newDaemonChB.push({ + topo: Object.assign({}, elem), + value: prevVal, + tooltip: 'Daemon binding ' + elem.name + ' (pid ' + elem.pid + ')', + }); + }); + + this.daemonCheckboxes = newDaemonChB; + } + + + // Compute the distinct nodes from the links. + // Based on http://bl.ocks.org/mbostock/1153292 + private graphAGLBindings(topo: AglTopology[]) { + + const ws_link: { [id: string]: string[] } = {}; + let ii = 1; + topo.forEach(elem => { + if (elem.name === 'null') { + elem.name = 'Daemon-' + String(ii++); + } + if (elem.ws_clients && elem.ws_clients instanceof Array) { + elem.ws_clients.forEach((ws: string) => { + if (ws_link[ws]) { + ws_link[ws].push(elem.name); + } else { + ws_link[ws] = [elem.name]; + } + }); + } + if (elem.ws_servers && elem.ws_servers instanceof Array) { + elem.ws_servers.forEach((ws: string) => { + if (ws_link[ws]) { + ws_link[ws].push(elem.name); + } else { + ws_link[ws] = [elem.name]; + } + }); + } + }); + + const nodes = {}; + this.links = []; + ii = 1; + topo.forEach(elem => { + let almostOne = false; + if (elem.ws_clients && elem.ws_clients.length) { + elem.ws_clients.forEach(wsCli => { + ws_link[wsCli].forEach(appName => { + if (appName !== elem.name) { + almostOne = true; + this.links.push({ source: elem.name, target: appName, type: 'ws-client' }); + } + }); + }); + } + if (elem.ws_servers && elem.ws_servers.length) { + elem.ws_servers.forEach(wsSvr => { + ws_link[wsSvr].forEach(appName => { + if (appName !== elem.name) { + almostOne = true; + this.links.push({ source: elem.name, target: appName, type: 'ws-server' }); + } + }); + }); + } + if (!almostOne) { + const name = '???-' + String(ii++); + this.links.push({ + source: elem.isServer ? name : elem.name, + target: elem.isServer ? elem.name : name, + type: 'not-connected', + }); + } + }); + + this.links.forEach(function (link) { + link.source = nodes[link.source] || (nodes[link.source] = { + name: link.source, + }); + link.target = nodes[link.target] || (nodes[link.target] = { + name: link.target, + }); + }); + + const width = this.document.getElementById('graph').clientWidth, + height = this.document.getElementById('graph').clientHeight; + + // Delete previous graph + if (this.svg) { + this.svg.remove(); + } + + // Create new graph + const force = d3.layout.force() + .nodes(d3.values(nodes)) + .links(this.links) + .size([width, height]) + .linkDistance(120) + .charge(-600) + .on('tick', tick) + .start(); + // const force = d3.forceSimulation() + + this.graph = d3.select('#graph'); + this.svg = this.graph.append('svg') + .attr('width', width) + .attr('height', height); + + // Define the div for the tooltip + /* + const divTooltip = d3.select('#graph').append('div') + .attr('class', 'tooltip') + .style('opacity', 0); + */ + + // Per-type markers, as they don't inherit styles. + this.svg.append('defs').selectAll('marker') + .data(['ws-server', 'ws-client', 'not-connected']) + .enter().append('marker') + .attr('id', function (d) { + return d; + }) + .attr('viewBox', '0 -5 10 10') + .attr('refX', 15) + .attr('refY', -1.5) + .attr('markerWidth', 12) + .attr('markerHeight', 12) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5'); + + const path = this.svg.append('g').selectAll('path') + .data(force.links()) + .enter().append('path') + .attr('class', function (d) { + return 'link ' + d.type; + }) + .attr('marker-end', function (d) { + return 'url(#' + d.type + ')'; + }); + + const circle = this.svg.append('g').selectAll('circle') + .data(force.nodes()) + .enter().append('circle') + .attr('r', 12) + .call(force.drag); + + const text = this.svg.append('g').selectAll('text') + .data(force.nodes()) + .enter().append('text') + .attr('x', 20) + .attr('y', '.31em') + .text(function (d) { + return d.name; + }); + + /* TODO - SEB + circle.on('mouseover', d => { + divTooltip.transition() + .duration(200) + .style('opacity', .9); + divTooltip.html('This is a Tooltip
' + d.close) + .style('left', (d3.event.pageX) + 'px') + .style('top', (d3.event.pageY - 28) + 'px'); + }); + + // Tooltip Object + const tooltip = d3.select('body') + .append('div').attr('id', 'tooltip') + .style('position', 'absolute') + .style('z-index', '10') + .style('visibility', 'hidden') + .text('a simple tooltip'); + */ + + // Use elliptical arc path segments to doubly-encode directionally. + function tick() { + path.attr('d', linkArc); + circle.attr('transform', transform); + text.attr('transform', transform); + } + + function linkArc(d) { + const dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy); + return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y; + } + + function transform(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + } + } +} diff --git a/webapp/src/app/pages/monitoring/monitoring.component.html b/webapp/src/app/pages/monitoring/monitoring.component.html new file mode 100644 index 0000000..c46817f --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring.component.html @@ -0,0 +1,69 @@ + + + +
+
+ +
+
diff --git a/webapp/src/app/pages/monitoring/monitoring.component.scss b/webapp/src/app/pages/monitoring/monitoring.component.scss new file mode 100644 index 0000000..a125e8d --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring.component.scss @@ -0,0 +1,83 @@ +@import '../../@theme/styles/themes'; +@import '~@nebular/theme/components/card/card.component.theme'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-card-body { + display: flex; + align-items: center; + } + .action-groups-header { + flex-basis: 20%; + color: nb-theme(card-header-fg-heading); + font-family: nb-theme(card-header-font-family); + font-size: nb-theme(card-header-font-size); + font-weight: nb-theme(card-header-font-weight); + } + .nb-actions { + flex-basis: 80%; + } + .right { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + order: 1; + flex-direction: row-reverse; + } + nb-actions > nb-action { + padding: 0; + } + nb-action { + i { + color: nb-theme(color-fg); + font-size: 1.5rem; + margin-right: 1rem; + } + span { + font-family: nb-theme(font-secondary); + font-weight: nb-theme(font-weight-bold); + color: nb-theme(color-fg-heading); + text-transform: uppercase; + } + button { + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } + } + } + @include media-breakpoint-down(md) { + nb-actions nb-action { + padding: 0 0.75rem; + } + } + @include media-breakpoint-down(sm) { + nb-card-body { + padding: 1rem; + } + nb-action { + font-size: 0.75rem; + i { + font-size: 2rem; + margin-right: 0.5rem; + } + } + } + @include media-breakpoint-down(is) { + nb-action i { + font-size: 1.75rem; + margin: 0; + } + span { + display: none; + } + } +} diff --git a/webapp/src/app/pages/monitoring/monitoring.component.ts b/webapp/src/app/pages/monitoring/monitoring.component.ts new file mode 100644 index 0000000..ccbe365 --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring.component.ts @@ -0,0 +1,172 @@ +/** +* @license +* Copyright (C) 2017-2019 "IoT.bzh" +* Author Sebastien Douheret +* +* 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 +* +* http://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. +*/ + +import { Component, OnInit, Input } from '@angular/core'; +import { NbThemeService } from '@nebular/theme'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +import { ConfigService, IConfig } from '../../@core-xds/services/config.service'; +import { MonitoringService } from '../../@core-xds/services/monitoring.service'; +import { AlertService } from '../../@core-xds/services/alert.service'; + +export interface GrafanaDashboard { + name: string; + shortname: string; + url?: string; + safeUrl?: SafeResourceUrl; +} + +export interface GrafanaPanel { + name: string; + index: string; + url?: string; + safeUrl?: SafeResourceUrl; +} + +@Component({ + selector: 'xds-monitoring', + styleUrls: ['./monitoring.component.scss'], + templateUrl: './monitoring.component.html', +}) + +export class MonitoringComponent implements OnInit { + + /* TODO bind tm_* and refresh in UI */ + @Input() theme = 'light'; + @Input() tm_from = 'now-60s'; + @Input() tm_to = 'now'; + @Input() refresh = '5s'; + @Input() scroll_factor = 10000; + @Input() zoom_factor = 100000; + + displayMode = 'dashboard'; + + private dashboards: Map = new Map([ + ['xds_monitoring', { name: 'AGL XDS Monitoring', shortname: 'agl-xds-monitoring' }], + ]); + + private panels: Map = new Map([ + ['table', { name: 'Monitoring traces table', index: '2' }], + ['evt_data_bytes', { name: 'Requests & Events per second', index: '5' }], + ['req_evts_per_sec', { name: 'Events Data bytes', index: '12' }], + ]); + + constructor( + private monitoringSvr: MonitoringService, + private alert: AlertService, + private themeService: NbThemeService, + private sanitizer: DomSanitizer, + private configSvr: ConfigService, + ) { + } + + Config: IConfig = {}; + + ngOnInit() { + this.configSvr.Conf$.subscribe(cfg => this.Config = cfg); + + this._initDashboard(); + this._initPanels(); + + this.themeService.onThemeChange().subscribe(tm => { + this.theme = (tm.name === 'cosmic') ? 'dark' : 'light'; + this.themeUpdate(); + }); + } + + getDashboard(name: string): SafeResourceUrl { + return this.dashboards.get(name).safeUrl; + } + + getPanel(name: string): SafeResourceUrl { + return this.panels.get(name).safeUrl; + } + + displayModeChange() { + if (this.displayMode === 'dashboard') { + this.displayMode = 'panels'; + } else { + this.displayMode = 'dashboard'; + } + } + + themeUpdate() { + this._initDashboard(); + this._initPanels(); + } + + timeChange(val: number) { + /* TODO: convert tm_from string to number + this.tm_from += val * this.scroll_factor; + this.tm_to += val * this.scroll_factor; + this._initPanels(); + */ + } + + zoomOut() { + /* TODO: convert tm_from string to number + this.tm_from -= this.zoom_factor; + this.tm_to += this.zoom_factor; + this._initPanels(); + */ + } + + private _initDashboard() { + // http://localhost:3000/d/Lbpwc6Iiz/agl-xds-monitoring?from=now-40s&to=now&refresh=5s + this.dashboards.forEach(dd => { + dd.url = this._buildDashboardUrl(dd.shortname, this.tm_from, this.tm_to, this.refresh, this.theme); + dd.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(dd.url); + }); + } + private _initPanels() { + this.panels.forEach(gg => { + gg.url = this._buildPanelUrl(gg.index, this.tm_from, this.tm_to, this.refresh, this.theme); + gg.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(gg.url); + }); + } + + private _buildDashboardUrl(sname, from, to, refresh, theme: string) { + // FIXME get sname from config to support several dashboards + let url = 'http://localhost:3000/d/Lbpwc6Iiz/' + sname; + if (this.Config.grafanaDashboardUrl !== '') { + url = this.Config.grafanaDashboardUrl; + } + url += '?orgId=1'; + if (from !== '') { url += '&from=' + from; } + if (to !== '') { url += '&to=' + to; } + if (theme !== '') { url += '&theme=' + theme; } + if (refresh !== '') { url += '&refresh=' + refresh; } + url += '&sidemenu=close'; + return url; + } + + private _buildPanelUrl(idx, from, to, refresh, theme: string) { + let url = 'http://localhost:3000/d-solo/Lbpwc6Iiz/agl-xds-monitoring'; + if (this.Config.grafanaDashboardUrl !== '') { + url = this.Config.grafanaDashboardUrl; + } + url += '?panelId=' + idx; + url += '&orgId=1'; + if (from !== '') { url += '&from=' + from; } + if (to !== '') { url += '&to=' + to; } + if (theme !== '') { url += '&theme=' + theme; } + if (refresh !== '') { url += '&refresh=' + refresh; } + url += '&sidemenu=close'; + return url; + } +} diff --git a/webapp/src/app/pages/monitoring/monitoring.module.ts b/webapp/src/app/pages/monitoring/monitoring.module.ts new file mode 100644 index 0000000..0f8331f --- /dev/null +++ b/webapp/src/app/pages/monitoring/monitoring.module.ts @@ -0,0 +1,37 @@ +/** +* @license +* Copyright (C) 2017-2018 "IoT.bzh" +* Author Sebastien Douheret +* +* 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 +* +* http://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. +*/ + +import { NgModule } from '@angular/core'; +import { ThemeModule } from '../../@theme/theme.module'; + +import { MonitoringComponent } from './monitoring.component'; +import { MonitoringConfigComponent } from './monitoring-config.component'; + + +@NgModule({ + imports: [ + ThemeModule, + ], + declarations: [ + MonitoringComponent, + MonitoringConfigComponent, + ], + entryComponents: [ + ], +}) +export class MonitoringModule { } diff --git a/webapp/src/app/pages/pages-menu.ts b/webapp/src/app/pages/pages-menu.ts index 230966d..771c798 100644 --- a/webapp/src/app/pages/pages-menu.ts +++ b/webapp/src/app/pages/pages-menu.ts @@ -86,17 +86,17 @@ export const MENU_ITEMS: NbMenuItem[] = [ ], }, { - title: 'Supervision / Monitoring', + title: 'Monitoring', icon: 'fa fa-bar-chart', - link: '/pages/supervision', + link: '/pages/monitoring', children: [ { title: 'Config', - link: '/pages/supervision/config', + link: '/pages/monitoring/config', }, { title: 'Graph', - link: '/pages/supervision/graph', + link: '/pages/monitoring/graph', }, ], }, diff --git a/webapp/src/app/pages/pages-routing.module.ts b/webapp/src/app/pages/pages-routing.module.ts index ae2ef4a..c63d496 100644 --- a/webapp/src/app/pages/pages-routing.module.ts +++ b/webapp/src/app/pages/pages-routing.module.ts @@ -27,8 +27,8 @@ import { SdkManagementComponent } from './sdks/sdk-management/sdk-management.com import { TargetsComponent } from './targets/targets.component'; import { TerminalsComponent } from './targets/terminals/terminals.component'; import { BuildComponent } from './build/build.component'; -import { SupervisionComponent } from './supervision/supervision.component'; -import { SupervisionConfigComponent } from './supervision/supervision-config.component'; +import { MonitoringComponent } from './monitoring/monitoring.component'; +import { MonitoringConfigComponent } from './monitoring/monitoring-config.component'; const routes: Routes = [{ path: '', @@ -55,11 +55,11 @@ const routes: Routes = [{ path: 'targets/term', component: TerminalsComponent, }, { - path: 'supervision/config', - component: SupervisionConfigComponent, + path: 'monitoring/config', + component: MonitoringConfigComponent, }, { - path: 'supervision/graph', - component: SupervisionComponent, + path: 'monitoring/graph', + component: MonitoringComponent, }, { path: 'config', loadChildren: './config/config.module#ConfigModule', diff --git a/webapp/src/app/pages/pages.module.ts b/webapp/src/app/pages/pages.module.ts index 5ffa8d6..3481599 100644 --- a/webapp/src/app/pages/pages.module.ts +++ b/webapp/src/app/pages/pages.module.ts @@ -26,7 +26,7 @@ import { DashboardModule } from './dashboard/dashboard.module'; import { BuildModule } from './build/build.module'; import { ProjectsModule } from './projects/projects.module'; import { SdksModule } from './sdks/sdks.module'; -import { SupervisionModule } from './supervision/supervision.module'; +import { MonitoringModule } from './monitoring/monitoring.module'; import { TargetsModule } from './targets/targets.module'; import { PagesRoutingModule } from './pages-routing.module'; import { NotificationsComponent } from './notifications/notifications.component'; @@ -49,7 +49,7 @@ const PAGES_COMPONENTS = [ SdksModule, ToasterModule, TargetsModule, - SupervisionModule, + MonitoringModule, ], declarations: [ ...PAGES_COMPONENTS, diff --git a/webapp/src/app/pages/supervision/supervision-config.component.html b/webapp/src/app/pages/supervision/supervision-config.component.html deleted file mode 100644 index 1fbcd70..0000000 --- a/webapp/src/app/pages/supervision/supervision-config.component.html +++ /dev/null @@ -1,45 +0,0 @@ -
-

Configuration

- -
-
- -
- - - - - -
-
-
-
-
-
-
- - -
-
-
- -
- {{wsCkx.name}} - -
-
- -
- - -
-
-
diff --git a/webapp/src/app/pages/supervision/supervision-config.component.scss b/webapp/src/app/pages/supervision/supervision-config.component.scss deleted file mode 100644 index 7b8d318..0000000 --- a/webapp/src/app/pages/supervision/supervision-config.component.scss +++ /dev/null @@ -1,140 +0,0 @@ -/* FIXME: not working due to workaround (ViewEncapsulation.None) for svg -@import '../../@theme/styles/themes'; -@import '~@nebular/theme/components/card/card.component.theme'; -@import '~bootstrap/scss/mixins/breakpoints'; -@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; -@include nb-install-component() { - nb-card-body { - display: flex; - align-items: center; - } - .action-groups-header { - flex-basis: 20%; - color: nb-theme(card-header-fg-heading); - font-family: nb-theme(card-header-font-family); - font-size: nb-theme(card-header-font-size); - font-weight: nb-theme(card-header-font-weight); - } - .nb-actions { - flex-basis: 80%; - } - .right { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - order: 1; - flex-direction: row-reverse; - } - nb-actions > nb-action { - padding: 0; - } - nb-action { - i { - color: nb-theme(color-fg); - font-size: 1.5rem; - margin-right: 1rem; - } - span { - font-family: nb-theme(font-secondary); - font-weight: nb-theme(font-weight-bold); - color: nb-theme(color-fg-heading); - text-transform: uppercase; - } - button { - margin: 0 auto; - padding: 0; - cursor: pointer; - border: none; - background: none; - display: flex; - align-items: center; - &:focus { - box-shadow: none; - outline: none; - } - } - } - @include media-breakpoint-down(md) { - nb-actions nb-action { - padding: 0 0.75rem; - } - } - @include media-breakpoint-down(sm) { - nb-card-body { - padding: 1rem; - } - nb-action { - font-size: 0.75rem; - i { - font-size: 2rem; - margin-right: 0.5rem; - } - } - } - @include media-breakpoint-down(is) { - nb-action i { - font-size: 1.75rem; - margin: 0; - } - span { - display: none; - } - } -} -*/ - -button#refresh-topo { - color: #a4abb3; - font-size: 1.5rem; - margin-right: 1rem; - margin: 0 auto; - padding: 0; - cursor: pointer; - border: none; - background: none; - display: flex; - align-items: center; - &:focus { - box-shadow: none; - outline: none; - } -} - -button#start-trace { - margin-top: 10px; - margin-left: 10px; -} - -button#stop-trace { - margin-top: 10px; - margin-left: 10px; -} - -#ws-client { - fill: green; -} - -svg#graph { - .link { - fill: none; - stroke: #666; - stroke-width: 1.5px; - } - .link.ws-client { - stroke: green; - } - .link.not-connected { - stroke-dasharray: 0, 2 1; - } - circle { - fill: #ccc; - stroke: #333; - stroke-width: 1.5px; - } - text { - font: 1rem sans-serif; - pointer-events: none; - text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; - } -} diff --git a/webapp/src/app/pages/supervision/supervision-config.component.ts b/webapp/src/app/pages/supervision/supervision-config.component.ts deleted file mode 100644 index e96b936..0000000 --- a/webapp/src/app/pages/supervision/supervision-config.component.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** -* @license -* Copyright (C) 2017-2018 "IoT.bzh" -* Author Sebastien Douheret -* -* 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 -* -* http://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. -*/ - -import { Component, OnInit, AfterViewInit, ViewEncapsulation } from '@angular/core'; -import { Injectable, Inject } from '@angular/core'; -import { DOCUMENT } from '@angular/common'; -import * as d3 from 'd3'; - -import { SupervisionService, AglTopology } from '../../@core-xds/services/supervision.service'; -import { AlertService } from '../../@core-xds/services/alert.service'; - -interface WsCheckbox { - name: string; - pid: number; - value: boolean; - disabled: boolean; - tooltip: string; -} - -@Component({ - selector: 'xds-supervision', - styleUrls: ['./supervision-config.component.scss'], - templateUrl: './supervision-config.component.html', - encapsulation: ViewEncapsulation.None, // workaround about https://github.com/angular/angular/issues/7845 -}) -export class SupervisionConfigComponent implements OnInit, AfterViewInit { - - daemonCheckboxes: WsCheckbox[] = []; - starting = false; - stopping = false; - - private graph: any; - private svg: any; - private links = []; - - constructor(@Inject(DOCUMENT) private document: Document, - private supervisorSvr: SupervisionService, - private alert: AlertService, - ) { - - } - - ngOnInit() { - - } - - ngAfterViewInit() { - this.getAGLTopo(); - } - - getAGLTopo() { - this.supervisorSvr.getTopo().subscribe(topo => { - this.graphAGLBindings(topo); - this.updateCheckboxes(topo); - }); - } - - onStartTrace() { - this.starting = true; - - const dmArr = []; - this.daemonCheckboxes.forEach(dm => dm.value && dmArr.push(dm.pid)); - - this.supervisorSvr.startTrace({ pids: dmArr }).subscribe(res => { - this.starting = false; - this.alert.info('Monitoring successfully started'); - }, err => { - this.starting = false; - this.alert.error(err); - }); - } - - onStopTrace() { - this.stopping = true; - this.supervisorSvr.stopTrace({}).subscribe(res => { - this.stopping = false; - this.alert.info('Monitoring successfully stopped'); - }, err => { - this.stopping = false; - this.alert.error(err); - }); - } - - isStartBtnDisable(): boolean { - return this.starting; - } - - isStopBtnDisable(): boolean { - return this.stopping; - } - - private updateCheckboxes(topo: AglTopology[]) { - this.daemonCheckboxes = []; - topo.forEach(elem => { - this.daemonCheckboxes.push({ - name: elem.name, - pid: elem.pid, - value: false, - disabled: false, - tooltip: 'Daemon ' + elem.name + ' (pid ' + elem.pid + ')', - }); - }); - - } - - - // Compute the distinct nodes from the links. - // Based on http://bl.ocks.org/mbostock/1153292 - private graphAGLBindings(topo: AglTopology[]) { - - const ws_link: { [id: string]: string[] } = {}; - let ii = 1; - topo.forEach(elem => { - if (elem.name === 'null') { - elem.name = 'Daemon-' + String(ii++); - } - if (elem.ws_clients && elem.ws_clients instanceof Array) { - elem.ws_clients.forEach((ws: string) => { - if (ws_link[ws]) { - ws_link[ws].push(elem.name); - } else { - ws_link[ws] = [elem.name]; - } - }); - } - if (elem.ws_servers && elem.ws_servers instanceof Array) { - elem.ws_servers.forEach((ws: string) => { - if (ws_link[ws]) { - ws_link[ws].push(elem.name); - } else { - ws_link[ws] = [elem.name]; - } - }); - } - }); - - const nodes = {}; - this.links = []; - ii = 1; - topo.forEach(elem => { - let almostOne = false; - if (elem.ws_clients && elem.ws_clients.length) { - elem.ws_clients.forEach(wsCli => { - ws_link[wsCli].forEach(appName => { - if (appName !== elem.name) { - almostOne = true; - this.links.push({ source: elem.name, target: appName, type: 'ws-client' }); - } - }); - }); - } - if (elem.ws_servers && elem.ws_servers.length) { - elem.ws_servers.forEach(wsSvr => { - ws_link[wsSvr].forEach(appName => { - if (appName !== elem.name) { - almostOne = true; - this.links.push({ source: elem.name, target: appName, type: 'ws-server' }); - } - }); - }); - } - if (!almostOne) { - const name = '???-' + String(ii++); - this.links.push({ - source: elem.isServer ? name : elem.name, - target: elem.isServer ? elem.name : name, - type: 'not-connected', - }); - } - }); - - this.links.forEach(function (link) { - link.source = nodes[link.source] || (nodes[link.source] = { - name: link.source, - }); - link.target = nodes[link.target] || (nodes[link.target] = { - name: link.target, - }); - }); - - const width = this.document.getElementById('graph').clientWidth, - height = this.document.getElementById('graph').clientHeight; - - // Delete previous graph - if (this.svg) { - this.svg.remove(); - } - - // Create new graph - const force = d3.layout.force() - .nodes(d3.values(nodes)) - .links(this.links) - .size([width, height]) - .linkDistance(120) - .charge(-600) - .on('tick', tick) - .start(); - // const force = d3.forceSimulation() - - this.graph = d3.select('#graph'); - this.svg = this.graph.append('svg') - .attr('width', width) - .attr('height', height); - - // Define the div for the tooltip - /* - const divTooltip = d3.select('#graph').append('div') - .attr('class', 'tooltip') - .style('opacity', 0); - */ - - // Per-type markers, as they don't inherit styles. - this.svg.append('defs').selectAll('marker') - .data(['ws-server', 'ws-client', 'not-connected']) - .enter().append('marker') - .attr('id', function (d) { - return d; - }) - .attr('viewBox', '0 -5 10 10') - .attr('refX', 15) - .attr('refY', -1.5) - .attr('markerWidth', 12) - .attr('markerHeight', 12) - .attr('orient', 'auto') - .append('path') - .attr('d', 'M0,-5L10,0L0,5'); - - const path = this.svg.append('g').selectAll('path') - .data(force.links()) - .enter().append('path') - .attr('class', function (d) { - return 'link ' + d.type; - }) - .attr('marker-end', function (d) { - return 'url(#' + d.type + ')'; - }); - - const circle = this.svg.append('g').selectAll('circle') - .data(force.nodes()) - .enter().append('circle') - .attr('r', 12) - .call(force.drag); - - const text = this.svg.append('g').selectAll('text') - .data(force.nodes()) - .enter().append('text') - .attr('x', 20) - .attr('y', '.31em') - .text(function (d) { - return d.name; - }); - - /* TODO - SEB - circle.on('mouseover', d => { - divTooltip.transition() - .duration(200) - .style('opacity', .9); - divTooltip.html('This is a Tooltip
' + d.close) - .style('left', (d3.event.pageX) + 'px') - .style('top', (d3.event.pageY - 28) + 'px'); - }); - - // Tooltip Object - const tooltip = d3.select('body') - .append('div').attr('id', 'tooltip') - .style('position', 'absolute') - .style('z-index', '10') - .style('visibility', 'hidden') - .text('a simple tooltip'); - */ - - // Use elliptical arc path segments to doubly-encode directionally. - function tick() { - path.attr('d', linkArc); - circle.attr('transform', transform); - text.attr('transform', transform); - } - - function linkArc(d) { - const dx = d.target.x - d.source.x, - dy = d.target.y - d.source.y, - dr = Math.sqrt(dx * dx + dy * dy); - return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y; - } - - function transform(d) { - return 'translate(' + d.x + ',' + d.y + ')'; - } - } -} diff --git a/webapp/src/app/pages/supervision/supervision.component.html b/webapp/src/app/pages/supervision/supervision.component.html deleted file mode 100644 index 0db8ec8..0000000 --- a/webapp/src/app/pages/supervision/supervision.component.html +++ /dev/null @@ -1,69 +0,0 @@ - - - -
-
- -
-
diff --git a/webapp/src/app/pages/supervision/supervision.component.scss b/webapp/src/app/pages/supervision/supervision.component.scss deleted file mode 100644 index a125e8d..0000000 --- a/webapp/src/app/pages/supervision/supervision.component.scss +++ /dev/null @@ -1,83 +0,0 @@ -@import '../../@theme/styles/themes'; -@import '~@nebular/theme/components/card/card.component.theme'; -@import '~bootstrap/scss/mixins/breakpoints'; -@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; -@include nb-install-component() { - nb-card-body { - display: flex; - align-items: center; - } - .action-groups-header { - flex-basis: 20%; - color: nb-theme(card-header-fg-heading); - font-family: nb-theme(card-header-font-family); - font-size: nb-theme(card-header-font-size); - font-weight: nb-theme(card-header-font-weight); - } - .nb-actions { - flex-basis: 80%; - } - .right { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - order: 1; - flex-direction: row-reverse; - } - nb-actions > nb-action { - padding: 0; - } - nb-action { - i { - color: nb-theme(color-fg); - font-size: 1.5rem; - margin-right: 1rem; - } - span { - font-family: nb-theme(font-secondary); - font-weight: nb-theme(font-weight-bold); - color: nb-theme(color-fg-heading); - text-transform: uppercase; - } - button { - margin: 0 auto; - padding: 0; - cursor: pointer; - border: none; - background: none; - display: flex; - align-items: center; - &:focus { - box-shadow: none; - outline: none; - } - } - } - @include media-breakpoint-down(md) { - nb-actions nb-action { - padding: 0 0.75rem; - } - } - @include media-breakpoint-down(sm) { - nb-card-body { - padding: 1rem; - } - nb-action { - font-size: 0.75rem; - i { - font-size: 2rem; - margin-right: 0.5rem; - } - } - } - @include media-breakpoint-down(is) { - nb-action i { - font-size: 1.75rem; - margin: 0; - } - span { - display: none; - } - } -} diff --git a/webapp/src/app/pages/supervision/supervision.component.ts b/webapp/src/app/pages/supervision/supervision.component.ts deleted file mode 100644 index 3fff2b7..0000000 --- a/webapp/src/app/pages/supervision/supervision.component.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** -* @license -* Copyright (C) 2017-2018 "IoT.bzh" -* Author Sebastien Douheret -* -* 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 -* -* http://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. -*/ - -import { Component, OnInit, Input } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { NbThemeService } from '@nebular/theme'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; - -import { ConfigService, IConfig } from '../../@core-xds/services/config.service'; -import { SupervisionService } from '../../@core-xds/services/supervision.service'; -import { AlertService } from '../../@core-xds/services/alert.service'; - -export interface GrafanaDashboard { - name: string; - shortname: string; - url?: string; - safeUrl?: SafeResourceUrl; -} - -export interface GrafanaPanel { - name: string; - index: string; - url?: string; - safeUrl?: SafeResourceUrl; -} - -@Component({ - selector: 'xds-supervision', - styleUrls: ['./supervision.component.scss'], - templateUrl: './supervision.component.html', -}) - -export class SupervisionComponent implements OnInit { - - /* TODO bind tm_* and refresh in UI */ - @Input() theme = 'light'; - @Input() tm_from = 'now-60s'; - @Input() tm_to = 'now'; - @Input() refresh = '5s'; - @Input() scroll_factor = 10000; - @Input() zoom_factor = 100000; - - displayMode = 'dashboard'; - - private dashboards: Map = new Map([ - ['xds_supervisor', { name: 'AGL XDS Supervisor', shortname: 'agl-xds-supervisor' }], - ]); - - private panels: Map = new Map([ - ['table', { name: 'Supervisor traces table', index: '2' }], - ['evt_data_bytes', { name: 'Requests & Events per second', index: '5' }], - ['req_evts_per_sec', { name: 'Events Data bytes', index: '12' }], - ]); - - constructor( - private supervisionSvr: SupervisionService, - private alert: AlertService, - private themeService: NbThemeService, - private sanitizer: DomSanitizer, - private configSvr: ConfigService, - ) { - } - - Config: IConfig = {}; - - ngOnInit() { - this.configSvr.Conf$.subscribe(cfg => this.Config = cfg); - - this._initDashboard(); - this._initPanels(); - - this.themeService.onThemeChange().subscribe(tm => { - this.theme = (tm.name === 'cosmic') ? 'dark' : 'light'; - this.themeUpdate(); - }); - } - - getDashboard(name: string): SafeResourceUrl { - return this.dashboards.get(name).safeUrl; - } - - getPanel(name: string): SafeResourceUrl { - return this.panels.get(name).safeUrl; - } - - displayModeChange() { - if (this.displayMode === 'dashboard') { - this.displayMode = 'panels'; - } else { - this.displayMode = 'dashboard'; - } - } - - themeUpdate() { - this._initDashboard(); - this._initPanels(); - } - - timeChange(val: number) { - /* TODO: convert tm_from string to number - this.tm_from += val * this.scroll_factor; - this.tm_to += val * this.scroll_factor; - this._initPanels(); - */ - } - - zoomOut() { - /* TODO: convert tm_from string to number - this.tm_from -= this.zoom_factor; - this.tm_to += this.zoom_factor; - this._initPanels(); - */ - } - - private _initDashboard() { - // http://localhost:3000/d/Lbpwc6Iiz/agl-xds-supervisor?from=now-40s&to=now&refresh=5s - this.dashboards.forEach(dd => { - dd.url = this._buildDashboardUrl(dd.shortname, this.tm_from, this.tm_to, this.refresh, this.theme); - dd.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(dd.url); - }); - } - private _initPanels() { - this.panels.forEach(gg => { - gg.url = this._buildPanelUrl(gg.index, this.tm_from, this.tm_to, this.refresh, this.theme); - gg.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(gg.url); - }); - } - - private _buildDashboardUrl(sname, from, to, refresh, theme: string) { - // FIXME get sname from config to support several dashboards - let url = 'http://localhost:3000/d/Lbpwc6Iiz/' + sname; - if (this.Config.grafanaDashboardUrl !== '') { - url = this.Config.grafanaDashboardUrl; - } - url += '?orgId=1'; - if (from !== '') { url += '&from=' + from; } - if (to !== '') { url += '&to=' + to; } - if (theme !== '') { url += '&theme=' + theme; } - if (refresh !== '') { url += '&refresh=' + refresh; } - url += '&sidemenu=close'; - return url; - } - - private _buildPanelUrl(idx, from, to, refresh, theme: string) { - let url = 'http://localhost:3000/d-solo/Lbpwc6Iiz/agl-xds-supervisor'; - if (this.Config.grafanaDashboardUrl !== '') { - url = this.Config.grafanaDashboardUrl; - } - url += '?panelId=' + idx; - url += '&orgId=1'; - if (from !== '') { url += '&from=' + from; } - if (to !== '') { url += '&to=' + to; } - if (theme !== '') { url += '&theme=' + theme; } - if (refresh !== '') { url += '&refresh=' + refresh; } - url += '&sidemenu=close'; - return url; - } -} diff --git a/webapp/src/app/pages/supervision/supervision.module.ts b/webapp/src/app/pages/supervision/supervision.module.ts deleted file mode 100644 index 4c1cb0b..0000000 --- a/webapp/src/app/pages/supervision/supervision.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** -* @license -* Copyright (C) 2017-2018 "IoT.bzh" -* Author Sebastien Douheret -* -* 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 -* -* http://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. -*/ - -import { NgModule } from '@angular/core'; -import { ThemeModule } from '../../@theme/theme.module'; - -import { SupervisionComponent } from './supervision.component'; -import { SupervisionConfigComponent } from './supervision-config.component'; - - -@NgModule({ - imports: [ - ThemeModule, - ], - declarations: [ - SupervisionComponent, - SupervisionConfigComponent, - ], - entryComponents: [ - ], -}) -export class SupervisionModule { } diff --git a/webapp/tslint.json b/webapp/tslint.json index 9bfdcb2..7a10b10 100644 --- a/webapp/tslint.json +++ b/webapp/tslint.json @@ -6,8 +6,12 @@ "trailing-comma": [ true, { - "multiline": "always", - "singleline": "never" + "multiline": { + "objects": "ignore", + "arrays": "always", + "functions": "ignore", + "typeLiterals": "ignore" + } } ], "arrow-return-shorthand": true, @@ -119,7 +123,10 @@ "component-selector": [ true, "element", - ["ngx", "xds"], + [ + "ngx", + "xds" + ], "kebab-case" ], "use-input-property-decorator": true, -- cgit 1.2.3-korg