diff --git a/internal/core/domain/host/addressinfo.go b/internal/core/domain/host/addressinfo.go index b2d5a72..1bfaed9 100644 --- a/internal/core/domain/host/addressinfo.go +++ b/internal/core/domain/host/addressinfo.go @@ -86,6 +86,7 @@ func (addr *AddressInfo) GetUrlPrometheus() (string, error) { return hostUrl.String(), nil } +// getProtocol returns the protocol based on the secure flag. func getProtocol(secure bool) string { protocol := protocolHTTP if secure { diff --git a/internal/core/domain/host/host.go b/internal/core/domain/host/host.go index a02603c..364417f 100644 --- a/internal/core/domain/host/host.go +++ b/internal/core/domain/host/host.go @@ -78,25 +78,29 @@ func (host *Host) SetStatus(rpc Host) { case enums.TableTypeValidator: if !metricsHost.Updated || metricsHost.Uptime == "" { host.Status = enums.StatusRed - return } - case enums.TableTypeNode, enums.TableTypeRPC: - if !metricsHost.Updated || metricsHost.TotalTransactionsBlocks == 0 || metricsHost.LatestCheckpoint == 0 || - metricsHost.TransactionsPerSecond == 0 && len(metricsHost.TransactionsHistory) == metrics.TransactionsPerSecondWindow || - metricsHost.TxSyncPercentage == 0 || metricsHost.TxSyncPercentage > 110 || metricsHost.CheckSyncPercentage > 110 { + case enums.TableTypeNode, enums.TableTypeRPC: + if !metricsHost.Updated { host.Status = enums.StatusRed + return + } + if metricsHost.TotalTransactionsBlocks == 0 || + metricsHost.LatestCheckpoint == 0 || + (metricsHost.TransactionsPerSecond == 0 && len(metricsHost.TransactionsHistory) == metrics.TransactionsPerSecondWindow) || + metricsHost.TxSyncPercentage == 0 || + metricsHost.TxSyncPercentage > 110 || + metricsHost.CheckSyncPercentage > 110 { + host.Status = enums.StatusRed return } if metricsHost.IsUnhealthy(enums.MetricTypeTransactionsPerSecond, metricsRPC.TransactionsPerSecond) || metricsHost.IsUnhealthy(enums.MetricTypeTotalTransactionBlocks, metricsRPC.TotalTransactionsBlocks) || metricsHost.IsUnhealthy(enums.MetricTypeLatestCheckpoint, metricsRPC.LatestCheckpoint) { - host.Status = enums.StatusYellow - return } } diff --git a/internal/core/domain/service/dashboardbuilder/init.go b/internal/core/domain/service/dashboardbuilder/init.go index 25a985e..fa1f291 100644 --- a/internal/core/domain/service/dashboardbuilder/init.go +++ b/internal/core/domain/service/dashboardbuilder/init.go @@ -16,7 +16,6 @@ import ( // new dashboard using the `container.New()` method. The dashboard instance is // stored in the `db.dashboard` field for later use. func (db *Builder) Init() (err error) { - // Use a deferred function to call db.TearDown() if there were errors or panics defer func() { if err != nil { db.tearDown() diff --git a/internal/core/domain/service/dashboardbuilder/render.go b/internal/core/domain/service/dashboardbuilder/render.go index 9b3e44e..9fe9724 100644 --- a/internal/core/domain/service/dashboardbuilder/render.go +++ b/internal/core/domain/service/dashboardbuilder/render.go @@ -16,34 +16,56 @@ const ( queryInterval = 2500 * time.Millisecond ) -// Render displays the dashboard on the terminal and updates the cells with new data periodically. +// Render renders the dashboard by starting the query and rerender loops, +// and waiting for them to complete. It returns an error if any of the loops +// encounter an error. func (db *Builder) Render() (err error) { - // Use a deferred function to call db.TearDown() if there were errors or panics defer func() { - if err != nil { + if r := recover(); r != nil { + db.cliGateway.Error(fmt.Sprintf("panic: %v", r)) db.tearDown() - } - - if err := recover(); err != nil { - // Handle the panic by logging the error and exiting the program - db.tearDown() - - db.cliGateway.Error(fmt.Sprintf("panic: %v", err)) - os.Exit(1) + } else if err != nil { + db.tearDown() } }() var errGroup errgroup.Group - tickerQuery := time.NewTicker(queryInterval) - defer tickerQuery.Stop() + queryTicker, renderTicker := startTickers() + defer stopTickers(queryTicker, renderTicker) + + errGroup.Go(queryMetricsLoop(db, queryTicker)) + errGroup.Go(rerenderLoop(db, renderTicker)) + errGroup.Go(runDashboard(db)) + + return errGroup.Wait() +} + +func startTickers() (queryTicker *time.Ticker, renderTicker *time.Ticker) { + queryTicker = time.NewTicker(queryInterval) + renderTicker = time.NewTicker(renderInterval) + return +} - // Start a goroutine for the metric retrieval loop - errGroup.Go(func() error { +func stopTickers(tickers ...*time.Ticker) { + for _, ticker := range tickers { + ticker.Stop() + } +} + +// queryMetricsLoop fetches the metrics from the host at regular intervals. +// It uses the provided ticker to trigger the fetch and returns an error if +// the fetch encounters an error or if the context is done. +// It returns a function that can be used to start the loop. +// The loop can be stopped by canceling the context. +// The function signature is compatible with the errgroup.Group.Go method. +// The loop stops when the context is done. +func queryMetricsLoop(db *Builder, ticker *time.Ticker) func() error { + return func() error { for { select { - case <-tickerQuery.C: + case <-ticker.C: if err := db.host.GetMetrics(); err != nil { return err } @@ -51,16 +73,19 @@ func (db *Builder) Render() (err error) { return nil } } - }) - - tickerRerender := time.NewTicker(renderInterval) - defer tickerRerender.Stop() + } +} - // Start a goroutine for the dashboard rendering loop - errGroup.Go(func() error { +// rerenderLoop continuously fetches the latest column values from the host at regular intervals. +// It uses the provided ticker to trigger the fetch and updates the cells with the latest values. +// The loop stops when the context is done. +// The function signature is compatible with the errgroup.Group.Go method. +// It returns a function that can be used to start the loop. +func rerenderLoop(db *Builder, ticker *time.Ticker) func() error { + return func() error { for { select { - case <-tickerRerender.C: + case <-ticker.C: columnValues, err := dashboards.GetColumnsValues(db.tableType, db.host) if err != nil { return err @@ -80,19 +105,19 @@ func (db *Builder) Render() (err error) { return nil } } - }) - - errGroup.Go(func() error { - // Display the dashboard on the terminal and handle errors - if err := termdash.Run( - db.ctx, db.terminal, db.dashboard, - termdash.KeyboardSubscriber(db.quitter), - ); err != nil { - return fmt.Errorf("failed to run terminal dashboard: %w", err) - } - - return nil - }) + } +} - return errGroup.Wait() +// runDashboard runs the dashboard using termdash.Run method. +// It takes a Builder instance as input and returns a function that can be used to start the dashboard. +// The returned function can be used to start the dashboard and handle keyboard events using the provided quitter function. +// It returns an error if the dashboard fails to run. +// The dashboard is run using the termdash.Run method with the provided context, terminal, dashboard, and keyboard subscriber. +// The returned error indicates any failure during the dashboard run. +// The function signature is compatible with the errgroup.Group.Go method. +// It returns a function that can be used to start the dashboard. +func runDashboard(db *Builder) func() error { + return func() error { + return termdash.Run(db.ctx, db.terminal, db.dashboard, termdash.KeyboardSubscriber(db.quitter)) + } } diff --git a/internal/core/domain/service/tablebuilder/init.go b/internal/core/domain/service/tablebuilder/init.go index 15cc608..3e0a14e 100644 --- a/internal/core/domain/service/tablebuilder/init.go +++ b/internal/core/domain/service/tablebuilder/init.go @@ -17,62 +17,66 @@ const utcTimeZone = "America/New_York" // Init initializes the table configuration based on the given table type and host data. // It processes the host data and calls the appropriate handler function for the specified table type. func (tb *Builder) Init() error { - hosts := tb.hosts - - if len(hosts) == 0 { + if len(tb.hosts) == 0 { return errors.New("hosts are not initialized") } - switch tb.tableType { - case enums.TableTypeNode: - tb.handleNodeTable(hosts) - case enums.TableTypeRPC: - tb.handleRPCTable(hosts) - case enums.TableTypeValidator: - tb.handleValidatorTable(hosts) - case enums.TableTypeGasPriceAndSubsidy: - metrics := hosts[0].Metrics - - return tb.handleSystemStateTable(&metrics) - case enums.TableTypeValidatorsParams: - systemState := hosts[0].Metrics.SystemState - - return tb.handleValidatorParamsTable(&systemState) - case enums.TableTypeValidatorsAtRisk: - systemState := hosts[0].Metrics.SystemState - - if err := tb.handleValidatorsAtRiskTable(&systemState); err != nil { - return err - } - case enums.TableTypeValidatorReports: - systemState := hosts[0].Metrics.SystemState - - if err := tb.handleValidatorReportsTable(&systemState); err != nil { - return err - } - case enums.TableTypeActiveValidators: - metrics := hosts[0].Metrics + handlerMap := map[enums.TableType]func([]domainhost.Host) error{ + enums.TableTypeNode: tb.handleNodeTable, + enums.TableTypeRPC: tb.handleRPCTable, + enums.TableTypeValidator: tb.handleValidatorTable, + enums.TableTypeGasPriceAndSubsidy: tb.handleSystemStateTableWrapper, + enums.TableTypeValidatorsParams: tb.handleValidatorParamsTableWrapper, + enums.TableTypeValidatorsAtRisk: tb.handleValidatorsAtRiskTableWrapper, + enums.TableTypeValidatorReports: tb.handleValidatorReportsTableWrapper, + enums.TableTypeActiveValidators: tb.handleActiveValidatorsTableWrapper, + } - return tb.handleActiveValidatorsTable(&metrics) + if handler, ok := handlerMap[tb.tableType]; ok { + return handler(tb.hosts) } return nil } +func (tb *Builder) handleSystemStateTableWrapper(hosts []domainhost.Host) error { + metrics := hosts[0].Metrics + return tb.handleSystemStateTable(&metrics) +} + +func (tb *Builder) handleValidatorParamsTableWrapper(hosts []domainhost.Host) error { + systemState := hosts[0].Metrics.SystemState + return tb.handleValidatorParamsTable(&systemState) +} + +func (tb *Builder) handleValidatorsAtRiskTableWrapper(hosts []domainhost.Host) error { + systemState := hosts[0].Metrics.SystemState + return tb.handleValidatorsAtRiskTable(&systemState) +} + +func (tb *Builder) handleValidatorReportsTableWrapper(hosts []domainhost.Host) error { + systemState := hosts[0].Metrics.SystemState + return tb.handleValidatorReportsTable(&systemState) +} + +func (tb *Builder) handleActiveValidatorsTableWrapper(hosts []domainhost.Host) error { + metrics := hosts[0].Metrics + return tb.handleActiveValidatorsTable(&metrics) +} + // handleNodeTable handles the configuration for the Node table. -func (tb *Builder) handleNodeTable(hosts []domainhost.Host) { +func (tb *Builder) handleNodeTable(hosts []domainhost.Host) error { tableConfig := tables.NewDefaultTableConfig(enums.TableTypeNode) - sort.SliceStable(hosts, func(left, right int) bool { - if hosts[left].Status != hosts[right].Status { - return hosts[left].Status > hosts[right].Status + sort.SliceStable(hosts, func(i, j int) bool { + left, right := hosts[i], hosts[j] + if left.Status != right.Status { + return left.Status > right.Status } - - if hosts[left].Metrics.TotalTransactionsBlocks != hosts[right].Metrics.TotalTransactionsBlocks { - return hosts[left].Metrics.TotalTransactionsBlocks > hosts[right].Metrics.TotalTransactionsBlocks + if left.Metrics.TotalTransactionsBlocks != right.Metrics.TotalTransactionsBlocks { + return left.Metrics.TotalTransactionsBlocks > right.Metrics.TotalTransactionsBlocks } - - return hosts[left].Metrics.HighestSyncedCheckpoint != hosts[right].Metrics.HighestSyncedCheckpoint + return left.Metrics.HighestSyncedCheckpoint > right.Metrics.HighestSyncedCheckpoint }) for idx, host := range hosts { @@ -83,23 +87,24 @@ func (tb *Builder) handleNodeTable(hosts []domainhost.Host) { columnValues := tables.GetNodeColumnValues(idx, host) tableConfig.Columns.SetColumnValues(columnValues) - tableConfig.RowsCount++ } tb.config = tableConfig + + return nil } // handleRPCTable handles the configuration for the RPC table. -func (tb *Builder) handleRPCTable(hosts []domainhost.Host) { +func (tb *Builder) handleRPCTable(hosts []domainhost.Host) error { tableConfig := tables.NewDefaultTableConfig(enums.TableTypeRPC) - sort.SliceStable(hosts, func(left, right int) bool { - if hosts[left].Status != hosts[right].Status { - return hosts[left].Status > hosts[right].Status + sort.SliceStable(hosts, func(i, j int) bool { + left, right := hosts[i], hosts[j] + if left.Status != right.Status { + return left.Status > right.Status } - - return hosts[left].Metrics.TotalTransactionsBlocks > hosts[right].Metrics.TotalTransactionsBlocks + return left.Metrics.TotalTransactionsBlocks > right.Metrics.TotalTransactionsBlocks }) for idx, host := range hosts { @@ -110,27 +115,27 @@ func (tb *Builder) handleRPCTable(hosts []domainhost.Host) { columnValues := tables.GetRPCColumnValues(idx, host) tableConfig.Columns.SetColumnValues(columnValues) - tableConfig.RowsCount++ } tb.config = tableConfig + + return nil } // handleValidatorTable handles the configuration for the Validator table. -func (tb *Builder) handleValidatorTable(hosts []domainhost.Host) { +func (tb *Builder) handleValidatorTable(hosts []domainhost.Host) error { tableConfig := tables.NewDefaultTableConfig(enums.TableTypeValidator) - sort.SliceStable(hosts, func(left, right int) bool { - if hosts[left].Status != hosts[right].Status { - return hosts[left].Status > hosts[right].Status + sort.SliceStable(hosts, func(i, j int) bool { + left, right := hosts[i], hosts[j] + if left.Status != right.Status { + return left.Status > right.Status } - - if hosts[left].Metrics.CurrentRound != hosts[right].Metrics.CurrentRound { - return hosts[left].Metrics.CurrentRound > hosts[right].Metrics.CurrentRound + if left.Metrics.CurrentRound != right.Metrics.CurrentRound { + return left.Metrics.CurrentRound > right.Metrics.CurrentRound } - - return hosts[left].Metrics.HighestSyncedCheckpoint > hosts[right].Metrics.HighestSyncedCheckpoint + return left.Metrics.HighestSyncedCheckpoint > right.Metrics.HighestSyncedCheckpoint }) for idx, host := range hosts { @@ -141,11 +146,12 @@ func (tb *Builder) handleValidatorTable(hosts []domainhost.Host) { columnValues := tables.GetValidatorColumnValues(idx, host) tableConfig.Columns.SetColumnValues(columnValues) - tableConfig.RowsCount++ } tb.config = tableConfig + + return nil } // handleSystemStateTable handles the configuration for the System State table. @@ -190,32 +196,28 @@ func (tb *Builder) handleValidatorsAtRiskTable(systemState *domainmetrics.SuiSys tableConfig := tables.NewDefaultTableConfig(enums.TableTypeValidatorsAtRisk) validatorsAtRisk := systemState.ValidatorsAtRiskParsed + const base = 10 - const base = 10 // for strconv.ParseInt - - sort.SliceStable(validatorsAtRisk, func(left, right int) bool { - epochsAtRiskLeft, err := strconv.ParseInt(validatorsAtRisk[left].EpochsAtRisk, base, 64) - if err != nil { - return true - } + // Optimized sorting logic + sort.SliceStable(validatorsAtRisk, func(i, j int) bool { + leftEpochs, leftErr := strconv.ParseInt(validatorsAtRisk[i].EpochsAtRisk, base, 64) + rightEpochs, rightErr := strconv.ParseInt(validatorsAtRisk[j].EpochsAtRisk, base, 64) - epochsAtRiskRight, err := strconv.ParseInt(validatorsAtRisk[right].EpochsAtRisk, base, 64) - if err != nil { - return true + if leftErr != nil || rightErr != nil { + return leftErr == nil } - if epochsAtRiskLeft != epochsAtRiskRight { - return epochsAtRiskLeft > epochsAtRiskRight + if leftEpochs != rightEpochs { + return leftEpochs > rightEpochs } - return validatorsAtRisk[left].Name < validatorsAtRisk[right].Name + return validatorsAtRisk[i].Name < validatorsAtRisk[j].Name }) for idx, validator := range validatorsAtRisk { columnValues := tables.GetValidatorAtRiskColumnValues(idx, validator) tableConfig.Columns.SetColumnValues(columnValues) - tableConfig.RowsCount++ } @@ -262,44 +264,44 @@ func (tb *Builder) handleActiveValidatorsTable(metrics *domainmetrics.Metrics) e activeValidators := metrics.SystemState.ActiveValidators validatorsApy := metrics.ValidatorsApyParsed - const base = 10 // for strconv.ParseInt + const base = 10 - sort.SliceStable(activeValidators, func(left, right int) bool { - votingPowerLeft, err := strconv.ParseInt(activeValidators[left].VotingPower, base, 64) - if err != nil { - return true - } + sort.SliceStable(activeValidators, func(i, j int) bool { + leftVotingPower, leftErr := strconv.ParseInt(activeValidators[i].VotingPower, base, 64) + rightVotingPower, rightErr := strconv.ParseInt(activeValidators[j].VotingPower, base, 64) - votingPowerRight, err := strconv.ParseInt(activeValidators[right].VotingPower, base, 64) - if err != nil { - return false // right is considered greater + if leftErr != nil { + return false } - - nextEpochStakeLeft, err := strconv.ParseInt(activeValidators[left].NextEpochStake, base, 64) - if err != nil { + if rightErr != nil { return true } - nextEpochStakeRight, err := strconv.ParseInt(activeValidators[right].NextEpochStake, base, 64) - if err != nil { - return false // right is considered greater + leftNextEpochStake, leftStakeErr := strconv.ParseInt(activeValidators[i].NextEpochStake, base, 64) + rightNextEpochStake, rightStakeErr := strconv.ParseInt(activeValidators[j].NextEpochStake, base, 64) + + if leftStakeErr != nil { + return false + } + if rightStakeErr != nil { + return true } - if votingPowerLeft != votingPowerRight { - return votingPowerLeft > votingPowerRight + if leftVotingPower != rightVotingPower { + return leftVotingPower > rightVotingPower } - if nextEpochStakeLeft != nextEpochStakeRight { - return nextEpochStakeLeft > nextEpochStakeRight + if leftNextEpochStake != rightNextEpochStake { + return leftNextEpochStake > rightNextEpochStake } - return activeValidators[left].Name < activeValidators[right].Name + return activeValidators[i].Name < activeValidators[j].Name }) for idx, validator := range activeValidators { validatorApy, ok := validatorsApy[validator.SuiAddress] if !ok { - return fmt.Errorf("failed to loookup validator apy by address: %s", validator.SuiAddress) + return fmt.Errorf("failed to lookup validator APY by address: %s", validator.SuiAddress) } validator.APY = strconv.FormatFloat(validatorApy*100, 'f', 3, 64)