├── .gitignore ├── static ├── images │ ├── suimon.png │ ├── suimon-help.gif │ ├── suimon-help.png │ ├── suimon-monitor.gif │ ├── suimon-monitor.png │ ├── suimon-version.gif │ ├── suimon-version.png │ ├── table-protocol.png │ ├── table-releases.png │ ├── suimon-table-type.png │ ├── table-full-nodes.png │ ├── table-validators.png │ ├── dashboard-full-nodes.png │ ├── dashboard-validators.png │ ├── suimon-monitor-type.png │ ├── table-epochs-history.png │ ├── table-reference-rpc.png │ ├── table-system-state.png │ ├── dashboard-system-state.png │ ├── dashboard-reference-rpc.png │ ├── table-active-validators.png │ ├── table-validators-params.png │ └── table-validators-reports.png └── templates │ ├── suimon-mainnet.yaml │ ├── suimon-testnet.yaml │ └── suimon-example.yaml ├── internal ├── core │ ├── ports │ │ ├── builder.go │ │ ├── command.go │ │ ├── controller.go │ │ └── gateway.go │ ├── domain │ │ ├── enums │ │ │ ├── porttype.go │ │ │ ├── widgettype.go │ │ │ ├── monitortype.go │ │ │ ├── prometheusmetrictype.go │ │ │ ├── rpcmethod.go │ │ │ ├── tabletype.go │ │ │ ├── status.go │ │ │ ├── prometheusmetricname.go │ │ │ └── metrictype.go │ │ ├── service │ │ │ ├── tablebuilder │ │ │ │ ├── render.go │ │ │ │ ├── handleProtocolTable.go │ │ │ │ ├── handleSystemStateTable.go │ │ │ │ ├── handleValidatorParamsTable.go │ │ │ │ ├── handleRpcTable.go │ │ │ │ ├── tables │ │ │ │ │ ├── row.go │ │ │ │ │ ├── column.go │ │ │ │ │ ├── rpc.go │ │ │ │ │ ├── release.go │ │ │ │ │ ├── peer.go │ │ │ │ │ ├── activevalidator.go │ │ │ │ │ ├── node.go │ │ │ │ │ └── base.go │ │ │ │ ├── handleReleasesTable.go │ │ │ │ ├── handleValidatorReportsTable.go │ │ │ │ ├── handleNodeTable.go │ │ │ │ ├── handleValidatorTable.go │ │ │ │ ├── handleValidatorAtRiskTable.go │ │ │ │ ├── handleActiveValidatorsTable.go │ │ │ │ ├── init.go │ │ │ │ └── base.go │ │ │ └── dashboardbuilder │ │ │ │ ├── dashboards │ │ │ │ ├── column.go │ │ │ │ ├── row.go │ │ │ │ ├── rpc.go │ │ │ │ ├── widget.go │ │ │ │ ├── node.go │ │ │ │ ├── base.go │ │ │ │ └── cell.go │ │ │ │ ├── base.go │ │ │ │ ├── init.go │ │ │ │ └── render.go │ │ ├── host │ │ │ ├── setlocation.go │ │ │ ├── addressinfo.go │ │ │ └── host.go │ │ ├── metrics │ │ │ ├── release.go │ │ │ ├── protocol.go │ │ │ ├── metrics.go │ │ │ └── getvalue.go │ │ └── config │ │ │ └── base.go │ ├── gateways │ │ ├── cligw │ │ │ ├── base.go │ │ │ ├── selectmany.go │ │ │ ├── selectchoice.go │ │ │ ├── selectone.go │ │ │ ├── error.go │ │ │ ├── warn.go │ │ │ └── info.go │ │ ├── prometheusgw │ │ │ ├── base.go │ │ │ └── callfor.go │ │ ├── rpcgw │ │ │ ├── base.go │ │ │ └── callfor.go │ │ └── geogw │ │ │ ├── base.go │ │ │ └── callfor.go │ ├── controllers │ │ ├── root.go │ │ ├── version.go │ │ └── monitor │ │ │ ├── renderdashboards.go │ │ │ ├── dynamic.go │ │ │ ├── rendertables.go │ │ │ ├── static.go │ │ │ ├── createhosts.go │ │ │ ├── base.go │ │ │ └── getaddressinfo.go │ └── handlers │ │ └── commands │ │ ├── roothandler.go │ │ ├── versionhandler.go │ │ ├── statichandler.go │ │ └── monitorhandler.go └── pkg │ ├── env │ └── env.go │ ├── log │ └── log.go │ ├── validation │ └── validation.go │ ├── progress │ └── progress.go │ └── address │ └── address.go ├── .github └── workflows │ └── release.yml ├── Taskfile.yml ├── LICENSE ├── .goreleaser.yaml ├── cmd └── main.go ├── .golangci.yml ├── go.mod └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .vscode/ 4 | 5 | # Build Outputs 6 | .build 7 | dist -------------------------------------------------------------------------------- /static/images/suimon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon.png -------------------------------------------------------------------------------- /static/images/suimon-help.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-help.gif -------------------------------------------------------------------------------- /static/images/suimon-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-help.png -------------------------------------------------------------------------------- /static/images/suimon-monitor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-monitor.gif -------------------------------------------------------------------------------- /static/images/suimon-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-monitor.png -------------------------------------------------------------------------------- /static/images/suimon-version.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-version.gif -------------------------------------------------------------------------------- /static/images/suimon-version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-version.png -------------------------------------------------------------------------------- /static/images/table-protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-protocol.png -------------------------------------------------------------------------------- /static/images/table-releases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-releases.png -------------------------------------------------------------------------------- /static/images/suimon-table-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-table-type.png -------------------------------------------------------------------------------- /static/images/table-full-nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-full-nodes.png -------------------------------------------------------------------------------- /static/images/table-validators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-validators.png -------------------------------------------------------------------------------- /internal/core/ports/builder.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | type Builder interface { 4 | Init() error 5 | Render() error 6 | } 7 | -------------------------------------------------------------------------------- /static/images/dashboard-full-nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/dashboard-full-nodes.png -------------------------------------------------------------------------------- /static/images/dashboard-validators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/dashboard-validators.png -------------------------------------------------------------------------------- /static/images/suimon-monitor-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/suimon-monitor-type.png -------------------------------------------------------------------------------- /static/images/table-epochs-history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-epochs-history.png -------------------------------------------------------------------------------- /static/images/table-reference-rpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-reference-rpc.png -------------------------------------------------------------------------------- /static/images/table-system-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-system-state.png -------------------------------------------------------------------------------- /static/images/dashboard-system-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/dashboard-system-state.png -------------------------------------------------------------------------------- /static/images/dashboard-reference-rpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/dashboard-reference-rpc.png -------------------------------------------------------------------------------- /static/images/table-active-validators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-active-validators.png -------------------------------------------------------------------------------- /static/images/table-validators-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-validators-params.png -------------------------------------------------------------------------------- /static/images/table-validators-reports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartosian/suimon/HEAD/static/images/table-validators-reports.png -------------------------------------------------------------------------------- /internal/core/domain/enums/porttype.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type PortType int 4 | 5 | const ( 6 | PortTypeRPC PortType = iota 7 | PortTypeMetrics 8 | ) 9 | -------------------------------------------------------------------------------- /internal/core/ports/command.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | type Command interface { 6 | Start() 7 | AddSubCommands(subcommands ...Command) 8 | Command() *cobra.Command 9 | } 10 | -------------------------------------------------------------------------------- /internal/core/domain/enums/widgettype.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type WidgetType int 4 | 5 | const ( 6 | WidgetTypeProgress WidgetType = iota 7 | WidgetTypeTextNoScroll 8 | WidgetTypeDisplay 9 | WidgetTypeSparkLine 10 | ) 11 | -------------------------------------------------------------------------------- /internal/core/domain/enums/monitortype.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type MonitorType string 4 | 5 | const ( 6 | MonitorTypeStatic MonitorType = "STATIC" 7 | MonitorTypeDynamic MonitorType = "DYNAMIC" 8 | ) 9 | 10 | func (e MonitorType) ToString() string { 11 | return string(e) 12 | } 13 | -------------------------------------------------------------------------------- /internal/core/ports/controller.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | type RootController interface { 4 | BeforeStart() bool 5 | } 6 | 7 | type VersionController interface { 8 | PrintVersion() 9 | } 10 | 11 | type MonitorController interface { 12 | Monitor() error 13 | Static() error 14 | Dynamic() error 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import "os" 4 | 5 | func GetEnvWithDefault(key, defaultValue string) string { 6 | if value, ok := os.LookupEnv(key); ok { 7 | return value 8 | } 9 | 10 | return defaultValue 11 | } 12 | 13 | func GetEnv(key string) string { 14 | return os.Getenv(key) 15 | } 16 | -------------------------------------------------------------------------------- /internal/core/domain/enums/prometheusmetrictype.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type PrometheusMetricType int 4 | 5 | const ( 6 | PrometheusMetricTypeCounter PrometheusMetricType = iota 7 | PrometheusMetricTypeGauge 8 | PrometheusMetricTypeHistogram 9 | PrometheusMetricTypeSummary 10 | PrometheusMetricTypeUntyped 11 | ) 12 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/render.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | // Render sets the rows, columns, and style for the Builder and then renders the table. 4 | func (tb *Builder) Render() error { 5 | if err := tb.setRows(); err != nil { 6 | return err 7 | } 8 | 9 | tb.setColumns() 10 | tb.setStyle() 11 | tb.writer.Render() 12 | 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/base.go: -------------------------------------------------------------------------------- 1 | package cligw 2 | 3 | import "github.com/AlecAivazis/survey/v2" 4 | 5 | type MsgOpts struct { 6 | Indent int 7 | } 8 | 9 | type Gateway struct { 10 | icons survey.AskOpt 11 | } 12 | 13 | func NewGateway() *Gateway { 14 | icons := survey.WithIcons(func(icons *survey.IconSet) { 15 | icons.Question.Text = "❔" 16 | }) 17 | 18 | return &Gateway{ 19 | icons: icons, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/core/controllers/root.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 5 | "github.com/bartosian/suimon/internal/core/ports" 6 | ) 7 | 8 | type RootController struct { 9 | cliGateway *cligw.Gateway 10 | } 11 | 12 | func NewRootController( 13 | cliGateway *cligw.Gateway, 14 | ) ports.RootController { 15 | return &RootController{ 16 | cliGateway: cliGateway, 17 | } 18 | } 19 | 20 | func (c *RootController) BeforeStart() bool { 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /internal/core/controllers/version.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 7 | "github.com/bartosian/suimon/internal/core/ports" 8 | ) 9 | 10 | const version = "v1.2.2" 11 | 12 | type VersionController struct { 13 | cliGateway *cligw.Gateway 14 | } 15 | 16 | func NewVersionController( 17 | cliGateway *cligw.Gateway, 18 | ) ports.VersionController { 19 | return &VersionController{ 20 | cliGateway: cliGateway, 21 | } 22 | } 23 | 24 | func (c *VersionController) PrintVersion() { 25 | slog.Info("Suimon version", "version", version) 26 | } 27 | -------------------------------------------------------------------------------- /internal/core/domain/enums/rpcmethod.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type RPCMethod string 4 | 5 | const ( 6 | RPCMethodGetTotalTransactionBlocks RPCMethod = "sui_getTotalTransactionBlocks" 7 | RPCMethodGetSuiSystemState RPCMethod = "suix_getLatestSuiSystemState" 8 | RPCMethodGetLatestCheckpointSequenceNumber RPCMethod = "sui_getLatestCheckpointSequenceNumber" 9 | RPCMethodGetValidatorsApy RPCMethod = "suix_getValidatorsApy" 10 | RPCMethodGetProtocol RPCMethod = "sui_getProtocolConfig" 11 | ) 12 | 13 | func (e RPCMethod) String() string { 14 | return string(e) 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // RemoveNonPrintableChars removes non-printable characters from the input string. 8 | // It takes a string as input and returns a new string with non-printable characters removed. 9 | // Non-printable characters are defined as any characters that are not visible when printed. 10 | // The function uses a regular expression to replace non-printable characters with an empty string. 11 | // It returns the modified string with only printable characters. 12 | func RemoveNonPrintableChars(str string) string { 13 | reg := regexp.MustCompile("[^[:print:]\n]") 14 | return reg.ReplaceAllString(str, "") 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Suimon Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | distribution: goreleaser 26 | version: latest 27 | args: release --clean 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /internal/core/gateways/prometheusgw/base.go: -------------------------------------------------------------------------------- 1 | package prometheusgw 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 9 | "github.com/bartosian/suimon/internal/core/ports" 10 | ) 11 | 12 | const httpClientTimeout = 3 * time.Second 13 | 14 | type Gateway struct { 15 | ctx context.Context 16 | client *http.Client 17 | cliGateway *cligw.Gateway 18 | url string 19 | } 20 | 21 | func NewGateway(cliGW *cligw.Gateway, url string) ports.PrometheusGateway { 22 | httpClient := http.Client{ 23 | Timeout: httpClientTimeout, 24 | } 25 | 26 | return &Gateway{ 27 | ctx: context.Background(), 28 | url: url, 29 | client: &httpClient, 30 | cliGateway: cliGW, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/pkg/validation/validation.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | const domainRegexp = `^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$` 10 | 11 | func IsValidDomain(domain string) bool { 12 | match, _ := regexp.MatchString(domainRegexp, domain) 13 | 14 | return match 15 | } 16 | 17 | func IsValidPort(port string) bool { 18 | portInt, err := strconv.Atoi(port) 19 | if err != nil { 20 | return false 21 | } 22 | 23 | return portInt >= 1 || portInt <= 65535 24 | } 25 | 26 | func IsInvalidPort(port string) bool { 27 | return !IsValidPort(port) 28 | } 29 | 30 | func IsValidCharCount(str, char string, count int) bool { 31 | charCount := strings.Count(str, char) 32 | 33 | return charCount == count 34 | } 35 | -------------------------------------------------------------------------------- /internal/core/domain/host/setlocation.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | const ( 9 | ErrInvalidIPAddressProvided = "invalid IP address: %v" 10 | ) 11 | 12 | // SetIPInfo sets the IPInfo property of a Host struct by calling an external geolocation API with the host's IP address. 13 | // It returns an error if the IP address is invalid or if the API call fails. 14 | func (host *Host) SetIPInfo() error { 15 | if host.Endpoint.IP == nil { 16 | return nil 17 | } 18 | 19 | ip := net.ParseIP(*host.Endpoint.IP) 20 | if ip == nil { 21 | return fmt.Errorf(ErrInvalidIPAddressProvided, host.Endpoint.IP) 22 | } 23 | 24 | ipInfo, err := host.gateways.geo.CallFor(ip) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | host.IPInfo = ipInfo 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/core/domain/enums/tabletype.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type TableType string 4 | 5 | const ( 6 | TableTypeRPC TableType = "📡 REFERENCE RPC" 7 | TableTypeNode TableType = "💻 FULL NODES" 8 | TableTypeValidator TableType = "🤖 VALIDATORS" 9 | TableTypeGasPriceAndSubsidy TableType = "💾 SYSTEM STATE" 10 | TableTypeProtocol TableType = "🌐 PROTOCOL" 11 | TableTypeValidatorParams TableType = "📊 VALIDATOR PARAMS" 12 | TableTypeValidatorsAtRisk TableType = "🚨 VALIDATORS AT RISK" 13 | TableTypeValidatorReports TableType = "📢 VALIDATOR REPORTS" 14 | TableTypeActiveValidators TableType = "✅ ACTIVE VALIDATORS" 15 | TableTypeReleases TableType = "📈 RELEASE HISTORY" 16 | ) 17 | 18 | func (e TableType) ToString() string { 19 | return string(e) 20 | } 21 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleProtocolTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "github.com/bartosian/suimon/internal/core/domain/enums" 5 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 6 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 7 | ) 8 | 9 | // handleProtocolTable handles the configuration for the Protocol table. 10 | func (tb *Builder) handleProtocolTable(metrics *domainmetrics.Metrics) error { 11 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeProtocol) 12 | 13 | columnValues, err := tables.GetProtocolColumnValues(metrics) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | tableConfig.Columns.SetColumnValues(columnValues) 19 | 20 | tableConfig.RowsCount++ 21 | 22 | tb.config = tableConfig 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/selectmany.go: -------------------------------------------------------------------------------- 1 | package cligw 2 | 3 | import "github.com/AlecAivazis/survey/v2" 4 | 5 | type SelectManyOpts struct{} 6 | 7 | func (gateway *Gateway) SelectMany(question string, choices SelectChoiceList) ([]SelectChoice, error) { 8 | return gateway.SelectManyWithOpts(question, choices, SelectManyOpts{}) 9 | } 10 | 11 | func (gateway *Gateway) SelectManyWithOpts(question string, choices SelectChoiceList, _ SelectManyOpts) ([]SelectChoice, error) { 12 | rawResult := new([]string) 13 | labels := choices.Labels() 14 | prompt := &survey.MultiSelect{ 15 | Message: question, 16 | Options: labels, 17 | PageSize: len(labels), 18 | } 19 | 20 | err := survey.AskOne(prompt, rawResult, gateway.icons) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return choices.GetByLabels(*rawResult...), nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleSystemStateTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "github.com/bartosian/suimon/internal/core/domain/enums" 5 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 6 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 7 | ) 8 | 9 | // handleSystemStateTable handles the configuration for the System State table. 10 | func (tb *Builder) handleSystemStateTable(metrics *domainmetrics.Metrics) error { 11 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeGasPriceAndSubsidy) 12 | 13 | columnValues, err := tables.GetSystemStateColumnValues(metrics) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | tableConfig.Columns.SetColumnValues(columnValues) 19 | 20 | tableConfig.RowsCount++ 21 | 22 | tb.config = tableConfig 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/renderdashboards.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import "fmt" 4 | 5 | // RenderDashboards displays the selected dashboard on the terminal. First, the function retrieves the name of the 6 | // currently selected dashboard. Then, it retrieves the corresponding dynamic dashboard builder from the controller's 7 | // `builders` map. If the builder is found, the function calls its `Render` method to render the dashboard. If the 8 | // builder is not found, the function returns an error. Finally, the function returns nil to indicate success. 9 | func (c *Controller) RenderDashboards() error { 10 | selectedDashboard := c.selectedDashboard 11 | 12 | builder := c.builders.dynamic[selectedDashboard] 13 | 14 | if err := builder.Render(); err != nil { 15 | return fmt.Errorf("error rendering dashboard %s: %w", selectedDashboard, err) 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleValidatorParamsTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "github.com/bartosian/suimon/internal/core/domain/enums" 5 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 6 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 7 | ) 8 | 9 | // handleValidatorParamsTable handles the configuration for the Validator Counts table. 10 | func (tb *Builder) handleValidatorParamsTable(systemState *domainmetrics.SuiSystemState) error { 11 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeValidatorParams) 12 | 13 | columnValues, err := tables.GetValidatorParamsColumnValues(systemState) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | tableConfig.Columns.SetColumnValues(columnValues) 19 | 20 | tableConfig.RowsCount++ 21 | 22 | tb.config = tableConfig 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/core/gateways/rpcgw/base.go: -------------------------------------------------------------------------------- 1 | package rpcgw 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/ybbus/jsonrpc/v3" 9 | 10 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 11 | "github.com/bartosian/suimon/internal/core/ports" 12 | ) 13 | 14 | const rpcClientTimeout = 3 * time.Second 15 | 16 | type Gateway struct { 17 | ctx context.Context 18 | client jsonrpc.RPCClient 19 | cliGateway *cligw.Gateway 20 | url string 21 | } 22 | 23 | func NewGateway(cliGW *cligw.Gateway, url string) ports.RPCGateway { 24 | httpClient := &http.Client{ 25 | Timeout: rpcClientTimeout, 26 | } 27 | 28 | opts := &jsonrpc.RPCClientOpts{ 29 | HTTPClient: httpClient, 30 | } 31 | 32 | rpcClient := jsonrpc.NewClientWithOpts(url, opts) 33 | 34 | return &Gateway{ 35 | ctx: context.Background(), 36 | url: url, 37 | client: rpcClient, 38 | cliGateway: cliGW, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /static/templates/suimon-mainnet.yaml: -------------------------------------------------------------------------------- 1 | # This section lists the reference public RPC endpoints that the client will use to monitor the network and assess the health of nodes and validators. 2 | # These endpoints serve as a benchmark against which the health of other full nodes is measured. 3 | # Please ensure that at least one working endpoint is provided. 4 | reference-rpc: 5 | - https://fullnode.mainnet.sui.io:443 6 | 7 | # if you wish to monitor the node, update this section with the node information 8 | full-nodes: 9 | 10 | # if you wish to monitor the validator, update this section with the validator information 11 | validators: 12 | 13 | # provider and country information in tables is requested from https://ipinfo.io/ public API. To use it, you need to obtain an access token on the website, 14 | # which is free and gives you 50k requests per month, which is sufficient for individual usage. 15 | ip-lookup: 16 | access-token: 55f30ce0213aa7 # temporary access token with requests limit 17 | -------------------------------------------------------------------------------- /static/templates/suimon-testnet.yaml: -------------------------------------------------------------------------------- 1 | # This section lists the reference public RPC endpoints that the client will use to monitor the network and assess the health of nodes and validators. 2 | # These endpoints serve as a benchmark against which the health of other full nodes is measured. 3 | # Please ensure that at least one working endpoint is provided. 4 | reference-rpc: 5 | - https://fullnode.testnet.sui.io:443 6 | 7 | # if you wish to monitor the node, update this section with the node information 8 | full-nodes: 9 | 10 | # if you wish to monitor the validator, update this section with the validator information 11 | validators: 12 | 13 | # provider and country information in tables is requested from https://ipinfo.io/ public API. To use it, you need to obtain an access token on the website, 14 | # which is free and gives you 50k requests per month, which is sufficient for individual usage. 15 | ip-lookup: 16 | access-token: 55f30ce0213aa7 # temporary access token with requests limit 17 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | run: 5 | desc: Run the suimon binary 6 | cmds: 7 | - go run cmd/main.go {{.CLI_ARGS}} 8 | 9 | run:monitor: 10 | desc: Run the suimon binary in monitor mode 11 | cmds: 12 | - go run cmd/main.go monitor 13 | 14 | build: 15 | desc: Build the suimon binary 16 | cmds: 17 | - go build -o .build/suimon cmd/main.go 18 | 19 | lint: 20 | desc: Lint the suimon code 21 | cmds: 22 | - golangci-lint run ./... 23 | 24 | update-deps: 25 | desc: Update Go dependencies 26 | cmds: 27 | - go get -u ./... 28 | - go mod tidy 29 | 30 | tag: 31 | desc: Create a new release tag 32 | vars: 33 | TAG: 34 | sh: | 35 | echo "Enter release tag:" && read tag && echo $tag 36 | MESSAGE: 37 | sh: | 38 | echo "Enter tag message:" && read message && echo $message 39 | cmds: 40 | - git tag -a "v{{.TAG}}" -m "{{.MESSAGE}}" 41 | - git push origin "v{{.TAG}}" -------------------------------------------------------------------------------- /internal/core/gateways/geogw/base.go: -------------------------------------------------------------------------------- 1 | package geogw 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/ipinfo/go/v2/ipinfo" 9 | "github.com/ipinfo/go/v2/ipinfo/cache" 10 | 11 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 12 | "github.com/bartosian/suimon/internal/core/ports" 13 | ) 14 | 15 | const ( 16 | ipInfoCacheExp = 5 * time.Minute 17 | httpClientTimeout = 4 * time.Second 18 | ) 19 | 20 | type Gateway struct { 21 | ctx context.Context 22 | client *ipinfo.Client 23 | cliGateway *cligw.Gateway 24 | accessToken string 25 | } 26 | 27 | func NewGateway(cliGW *cligw.Gateway, accessToken string) ports.GeoGateway { 28 | httpClient := &http.Client{Timeout: httpClientTimeout} 29 | infoCache := ipinfo.NewCache(cache.NewInMemory().WithExpiration(ipInfoCacheExp)) 30 | geoClient := ipinfo.NewClient(httpClient, infoCache, accessToken) 31 | 32 | return &Gateway{ 33 | ctx: context.Background(), 34 | accessToken: accessToken, 35 | client: geoClient, 36 | cliGateway: cliGW, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/core/gateways/geogw/callfor.go: -------------------------------------------------------------------------------- 1 | package geogw 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/bartosian/suimon/internal/core/ports" 8 | ) 9 | 10 | func (gateway *Gateway) CallFor(ip net.IP) (result *ports.IPResult, err error) { 11 | if ip == nil { 12 | return nil, fmt.Errorf("no IP provided") 13 | } 14 | 15 | data, err := gateway.client.GetIPInfo(ip) 16 | if err != nil { 17 | return nil, fmt.Errorf("failed to get IP data for %s: %w", ip, err) 18 | } 19 | 20 | company := new(ports.Company) 21 | 22 | if data.Company != nil { 23 | company = &ports.Company{ 24 | Name: data.Company.Name, 25 | Domain: data.Company.Domain, 26 | Type: data.Company.Type, 27 | } 28 | } 29 | 30 | return &ports.IPResult{ 31 | IP: data.IP, 32 | Hostname: data.Hostname, 33 | City: data.City, 34 | Region: data.Region, 35 | Country: data.Country, 36 | CountryName: data.CountryName, 37 | CountryEmoji: data.CountryFlag.Emoji, 38 | Location: data.Location, 39 | Company: company, 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/selectchoice.go: -------------------------------------------------------------------------------- 1 | package cligw 2 | 3 | type ( 4 | SelectChoiceList []SelectChoice 5 | SelectChoice struct { 6 | Data any 7 | Label string 8 | Value string 9 | } 10 | ) 11 | 12 | func NewSelectChoiceList(values ...string) SelectChoiceList { 13 | options := make(SelectChoiceList, 0, len(values)) 14 | 15 | for _, val := range values { 16 | option := SelectChoice{ 17 | Label: val, 18 | Value: val, 19 | } 20 | 21 | options = append(options, option) 22 | } 23 | 24 | return options 25 | } 26 | 27 | func (choiceList *SelectChoiceList) Labels() (result []string) { 28 | for _, option := range *choiceList { 29 | result = append(result, option.Label) 30 | } 31 | 32 | return 33 | } 34 | 35 | func (choiceList *SelectChoiceList) GetByLabels(labels ...string) SelectChoiceList { 36 | options := make(SelectChoiceList, 0) 37 | 38 | for _, label := range labels { 39 | for _, option := range *choiceList { 40 | if option.Label == label { 41 | options = append(options, option) 42 | } 43 | } 44 | } 45 | 46 | return options 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BartestneT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/selectone.go: -------------------------------------------------------------------------------- 1 | package cligw 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/AlecAivazis/survey/v2" 7 | ) 8 | 9 | type SelectOneOpts struct{ PageLimit *int } 10 | 11 | func (gateway *Gateway) SelectOne(question string, choices SelectChoiceList) (*SelectChoice, error) { 12 | return gateway.SelectOneWithOpts(question, choices, SelectOneOpts{}) 13 | } 14 | 15 | func (gateway *Gateway) SelectOneWithOpts( 16 | question string, 17 | choices SelectChoiceList, 18 | opts SelectOneOpts, 19 | ) (*SelectChoice, error) { 20 | rawResult := new(string) 21 | labels := choices.Labels() 22 | pageSize := len(labels) 23 | 24 | if opts.PageLimit != nil && (*opts.PageLimit) < pageSize { 25 | pageSize = *opts.PageLimit 26 | } 27 | 28 | prompt := &survey.Select{ 29 | Message: question, 30 | Options: labels, 31 | PageSize: pageSize, 32 | } 33 | 34 | err := survey.AskOne(prompt, rawResult, gateway.icons) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | results := choices.GetByLabels(*rawResult) 40 | 41 | if len(results) == 0 { 42 | return nil, errors.New("no result selected") 43 | } 44 | 45 | return &results[0], nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/error.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl // temporary disabled 2 | package cligw 3 | 4 | import ( 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | const errorIcon = "❗" 12 | 13 | var ( 14 | iconErrColor = color.New(color.FgRed, color.Bold) 15 | messageErrColor = color.New(color.FgWhite, color.Bold) 16 | ) 17 | 18 | func (gateway *Gateway) Error(msg string) { 19 | gateway.ErrorWithOpts(msg, MsgOpts{}) 20 | } 21 | 22 | func (gateway *Gateway) Errorf(msg string, vars ...interface{}) { 23 | gateway.ErrorfWithOpts(msg, MsgOpts{}, vars) 24 | } 25 | 26 | func (gateway *Gateway) ErrorfWithOpts(msg string, opts MsgOpts, vars ...interface{}) { 27 | msg = fmt.Sprintf(msg, vars...) 28 | 29 | gateway.ErrorWithOpts(msg, opts) 30 | } 31 | 32 | func (Gateway) ErrorWithOpts(msg string, opts MsgOpts) { 33 | var newIcon string 34 | 35 | for icon, i := errorIcon, opts.Indent; i > 0; i-- { 36 | newIcon = fmt.Sprintf(" %s", icon) 37 | } 38 | 39 | formattedIcon := iconErrColor.Sprint(newIcon) 40 | formattedMsg := messageErrColor.Sprint(msg) 41 | 42 | result := fmt.Sprintf("%s %s", formattedIcon, formattedMsg) 43 | 44 | slog.Error(result) 45 | } 46 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/warn.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl // these files are different. 2 | package cligw 3 | 4 | import ( 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | const warnIcon = "⚠️" 12 | 13 | var ( 14 | iconWarnColor = color.New(color.FgYellow, color.Bold) 15 | messageWarnColor = color.New(color.FgWhite, color.Bold) 16 | ) 17 | 18 | func (gateway *Gateway) Warn(msg string) { 19 | gateway.WarnWithOpts(msg, MsgOpts{}) 20 | } 21 | 22 | func (gateway *Gateway) Warnf(msg string, vars ...interface{}) { 23 | gateway.WarnfWithOpts(msg, MsgOpts{}, vars) 24 | } 25 | 26 | func (gateway *Gateway) WarnfWithOpts(msg string, opts MsgOpts, vars ...interface{}) { 27 | msg = fmt.Sprintf(msg, vars) 28 | 29 | gateway.WarnWithOpts(msg, opts) 30 | } 31 | 32 | func (Gateway) WarnWithOpts(msg string, opts MsgOpts) { 33 | var newIcon string 34 | 35 | for icon, i := warnIcon, opts.Indent; i > 0; i-- { 36 | newIcon = fmt.Sprintf(" %s", icon) 37 | } 38 | 39 | formattedIcon := iconWarnColor.Sprint(newIcon) 40 | formattedMsg := messageWarnColor.Sprint(msg) 41 | 42 | result := fmt.Sprintf("%s %s", formattedIcon, formattedMsg) 43 | 44 | slog.Warn(result) 45 | } 46 | -------------------------------------------------------------------------------- /internal/core/gateways/cligw/info.go: -------------------------------------------------------------------------------- 1 | package cligw 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | const infoIcon = "ℹ️ " 11 | 12 | var ( 13 | iconInfoColor = color.New(color.FgGreen, color.Bold) 14 | messageInfoColor = color.New(color.FgWhite, color.Bold) 15 | ) 16 | 17 | func (gateway *Gateway) Info(label, value string) { 18 | gateway.InfoWithOpts(label, value, MsgOpts{}) 19 | } 20 | 21 | func (gateway *Gateway) InfoWithOpts(label, value string, opts MsgOpts) { 22 | labelLine := gateway.infoLabel(label, opts.Indent) 23 | 24 | valueLine := "" 25 | if value != "" { 26 | valueLine = fmt.Sprintf("%s %s", color.New(color.FgGreen, color.Bold).Sprint("->"), value) 27 | } 28 | 29 | result := fmt.Sprintf("%s %s", labelLine, valueLine) 30 | 31 | slog.Info(result) 32 | } 33 | 34 | func (Gateway) infoLabel(label string, indent int) string { 35 | var newIcon string 36 | 37 | for icon, i := infoIcon, indent; i > 0; i-- { 38 | newIcon = fmt.Sprintf(" %s", icon) 39 | } 40 | 41 | bang := iconInfoColor.Sprint(newIcon) 42 | formattedLabel := messageInfoColor.Sprint(label) 43 | 44 | return fmt.Sprintf("%s %s", bang, formattedLabel) 45 | } 46 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleRpcTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 8 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 9 | ) 10 | 11 | // handleRPCTable handles the configuration for the RPC table. 12 | func (tb *Builder) handleRPCTable(hosts []domainhost.Host) error { 13 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeRPC) 14 | 15 | sort.SliceStable(hosts, func(i, j int) bool { 16 | left, right := hosts[i], hosts[j] 17 | if left.Status != right.Status { 18 | return left.Status > right.Status 19 | } 20 | 21 | return left.Metrics.TotalTransactionsBlocks > right.Metrics.TotalTransactionsBlocks 22 | }) 23 | 24 | for idx := range hosts { 25 | host := hosts[idx] 26 | 27 | if !host.Metrics.Updated { 28 | continue 29 | } 30 | 31 | columnValues := tables.GetRPCColumnValues(idx, &host) 32 | 33 | tableConfig.Columns.SetColumnValues(columnValues) 34 | 35 | tableConfig.RowsCount++ 36 | } 37 | 38 | tb.config = tableConfig 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/row.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "github.com/jedib0t/go-pretty/v6/table" 5 | "github.com/jedib0t/go-pretty/v6/text" 6 | ) 7 | 8 | // Row represents a row in a table. 9 | type Row struct { 10 | Values []any 11 | Config table.RowConfig 12 | IsHeader bool 13 | IsFooter bool 14 | } 15 | 16 | type NewRowConfig struct { 17 | IsHeader bool 18 | IsFooter bool 19 | Length int 20 | AutoMerge bool 21 | AutoMergeAlign text.Align 22 | } 23 | 24 | // NewRow creates a new row based on the provided configuration. 25 | func NewRow(config NewRowConfig) Row { 26 | return Row{ 27 | Values: make(table.Row, 0, config.Length), 28 | Config: table.RowConfig{AutoMerge: config.AutoMerge, AutoMergeAlign: config.AutoMergeAlign}, 29 | IsHeader: config.IsHeader, 30 | IsFooter: config.IsFooter, 31 | } 32 | } 33 | 34 | // AppendValue appends a value to the end of the row. 35 | func (row *Row) AppendValue(value any) { 36 | row.Values = append(row.Values, value) 37 | } 38 | 39 | // PrependValue prepends a value to the beginning of the row. 40 | func (row *Row) PrependValue(value any) { 41 | row.Values = append(table.Row{value}, row.Values...) 42 | } 43 | -------------------------------------------------------------------------------- /internal/core/domain/enums/status.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jedib0t/go-pretty/v6/text" 7 | ) 8 | 9 | const statusWidgetLength = 100 10 | 11 | type Status string 12 | 13 | const ( 14 | StatusGreen Status = "\U0001F7E9" 15 | StatusYellow Status = "\U0001F7E8" 16 | StatusRed Status = "\U0001F7E5" 17 | StatusGrey Status = "\U0001F7E4" 18 | ) 19 | 20 | func (i Status) StatusToPlaceholder() string { 21 | return i.ColorStatus() 22 | } 23 | 24 | func (i Status) ColorStatus() string { 25 | colors := text.Colors{text.Bold} 26 | 27 | switch i { 28 | case StatusRed: 29 | colors = append(colors, text.BgRed, text.FgRed) 30 | case StatusYellow: 31 | colors = append(colors, text.BgYellow, text.FgYellow) 32 | case StatusGreen: 33 | colors = append(colors, text.BgGreen, text.FgGreen) 34 | case StatusGrey: 35 | } 36 | 37 | return colors.Sprint("| |") 38 | } 39 | 40 | func (i Status) DashboardStatus() string { 41 | statusWidget := make([]string, statusWidgetLength) 42 | 43 | repeatedPattern := strings.Repeat(" ", statusWidgetLength) 44 | 45 | for idx := range statusWidget { 46 | statusWidget[idx] = repeatedPattern 47 | } 48 | 49 | return strings.Join(statusWidget, "\n") 50 | } 51 | -------------------------------------------------------------------------------- /internal/core/ports/gateway.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | ) 10 | 11 | type RPCGateway interface { 12 | CallFor(method enums.RPCMethod, params ...interface{}) (result any, err error) 13 | } 14 | 15 | type PrometheusGateway interface { 16 | CallFor(metrics Metrics) (result MetricsResult, err error) 17 | } 18 | 19 | type GeoGateway interface { 20 | CallFor(ip net.IP) (result *IPResult, err error) 21 | } 22 | 23 | type MetricResult struct { 24 | Labels prometheus.Labels 25 | Value float64 26 | } 27 | 28 | type MetricsResult map[enums.PrometheusMetricName]MetricResult 29 | 30 | type MetricConfig struct { 31 | Labels prometheus.Labels 32 | MetricType enums.PrometheusMetricType 33 | } 34 | 35 | type Metrics map[enums.PrometheusMetricName]MetricConfig 36 | 37 | type Company struct { 38 | Name string 39 | Domain string 40 | Type string 41 | } 42 | 43 | type IPResult struct { 44 | Company *Company 45 | Hostname string 46 | City string 47 | Region string 48 | Country string 49 | CountryName string 50 | CountryEmoji string 51 | Location string 52 | IP net.IP 53 | } 54 | -------------------------------------------------------------------------------- /internal/core/handlers/commands/roothandler.go: -------------------------------------------------------------------------------- 1 | package cmdhandlers 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/bartosian/suimon/internal/core/ports" 7 | ) 8 | 9 | type RootHandler struct { 10 | command *cobra.Command 11 | controller ports.RootController 12 | } 13 | 14 | func NewRootHandler( 15 | controller ports.RootController, 16 | ) *RootHandler { 17 | handler := &RootHandler{ 18 | controller: controller, 19 | } 20 | 21 | handler.command = newCommand() 22 | 23 | return handler 24 | } 25 | 26 | func (h *RootHandler) Start() { 27 | _ = h.command.Execute() 28 | } 29 | 30 | func (h *RootHandler) AddSubCommands(subcommands ...ports.Command) { 31 | for _, subcommand := range subcommands { 32 | h.command.AddCommand(subcommand.Command()) 33 | } 34 | } 35 | 36 | func newCommand() *cobra.Command { 37 | return &cobra.Command{ 38 | Use: "suimon", 39 | Short: "Get real-time insights of SUI nodes and network performance", 40 | Long: "A comprehensive monitoring tool designed to provide real-time performance for SUI nodes and networks.\nWith an easy-to-install and user-friendly YAML configuration file, users can easily monitor network traffic, checkpoints, transactions, uptime, network status, peers, remote RPC, and more.\nFor help, use 'suimon --help'", 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/core/gateways/rpcgw/callfor.go: -------------------------------------------------------------------------------- 1 | package rpcgw 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/enums" 8 | ) 9 | 10 | type responseWithError struct { 11 | response any 12 | err error 13 | } 14 | 15 | // CallFor makes an RPC call for the specified method with the given parameters. 16 | // It returns the result of the RPC call and an error if any. 17 | func (gateway *Gateway) CallFor(method enums.RPCMethod, params ...interface{}) (result any, err error) { 18 | respChan := make(chan responseWithError) 19 | 20 | ctx, cancel := context.WithTimeout(gateway.ctx, rpcClientTimeout) 21 | defer cancel() 22 | 23 | go func() { 24 | var resp any 25 | 26 | callErr := gateway.client.CallFor(ctx, &resp, method.String(), params) 27 | 28 | if callErr != nil || resp == nil { 29 | respChan <- responseWithError{response: nil, err: fmt.Errorf("failed to get response from RPC client: %w", err)} 30 | } else { 31 | respChan <- responseWithError{response: resp, err: nil} 32 | } 33 | }() 34 | 35 | select { 36 | case <-ctx.Done(): 37 | return nil, fmt.Errorf("rpc call timed out: %w", ctx.Err()) 38 | case result := <-respChan: 39 | if result.err != nil { 40 | return nil, result.err 41 | } 42 | 43 | return result.response, nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleReleasesTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "github.com/bartosian/suimon/internal/core/domain/enums" 5 | "github.com/bartosian/suimon/internal/core/domain/metrics" 6 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 7 | ) 8 | 9 | // handleReleasesTable processes the provided releases and builds a table configuration for them. 10 | // It iterates over each release, retrieves the column values for it, and sets these values in the table configuration. 11 | // The function also increments the row count for each processed release. 12 | // If an error occurs while getting the column values, the function returns the error. 13 | // At the end, the built table configuration is set as the builder's configuration. 14 | // The function returns nil if it completes successfully. 15 | func (tb *Builder) handleReleasesTable(releases []metrics.Release) error { 16 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeReleases) 17 | 18 | for idx := range releases { 19 | release := releases[idx] 20 | 21 | columnValues := tables.GetReleaseColumnValues(idx, &release) 22 | 23 | tableConfig.Columns.SetColumnValues(columnValues) 24 | 25 | tableConfig.RowsCount++ 26 | } 27 | 28 | tb.config = tableConfig 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /static/templates/suimon-example.yaml: -------------------------------------------------------------------------------- 1 | # This section lists the reference public RPC endpoints that the client will use to monitor the network and assess the health of nodes and validators. 2 | # These endpoints serve as a benchmark against which the health of other full nodes is measured. 3 | # Please ensure that at least one working endpoint is provided. 4 | reference-rpc: 5 | - https://rpc-ws-testnet-w3.suiprovider.xyz:443 6 | - https://sui-api.rpc.com:443 7 | 8 | # if you wish to monitor the node, update this section with the node information 9 | full-nodes: 10 | - json-rpc-address: 0.0.0.0:9000 11 | metrics-address: 0.0.0.0:9184 12 | - json-rpc-address: https://sui-rpc.testnet.com 13 | metrics-address: https://sui-rpc.testnet.com/metrics 14 | 15 | # if you wish to monitor the validator, update this section with the validator information 16 | validators: 17 | - metrics-address: 0.0.0.0:9184/metrics 18 | - metrics-address: https://sui-validator.testnet.com:9184/metrics 19 | 20 | # provider and country information in tables is requested from https://ipinfo.io/ public API. To use it, you need to obtain an access token on the website, 21 | # which is free and gives you 50k requests per month, which is sufficient for individual usage. 22 | ip-lookup: 23 | access-token: 55f30ce0213aa7 # temporary access token with requests limit 24 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleValidatorReportsTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 8 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 9 | ) 10 | 11 | // handleValidatorReportsTable handles the configuration for the Validator Reports table. 12 | // It takes the system state, extracts the necessary data, and updates the table configuration. 13 | func (tb *Builder) handleValidatorReportsTable(systemState *domainmetrics.SuiSystemState) error { 14 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeValidatorReports) 15 | 16 | validatorReports := systemState.ValidatorReportsParsed 17 | 18 | for _, report := range validatorReports { 19 | for j, reporter := range report.Reporters { 20 | reportedName := report.Name 21 | slashingPct := fmt.Sprintf("%.2f", report.SlashingPercentage) 22 | 23 | if j > 0 { 24 | reportedName = " " 25 | slashingPct = " " 26 | } 27 | 28 | columnValues := tables.GetValidatorReportColumnValues(reportedName, slashingPct, reporter) 29 | 30 | tableConfig.Columns.SetColumnValues(columnValues) 31 | 32 | tableConfig.RowsCount++ 33 | } 34 | } 35 | 36 | tb.config = tableConfig 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | project_name: suimon 4 | 5 | dist: dist 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | main: ./cmd/main.go 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | binary: suimon 16 | 17 | archives: 18 | - format: tar.gz 19 | name_template: >- 20 | {{ .ProjectName }}_ 21 | {{- title .Os }}_ 22 | {{- if eq .Arch "amd64" }}x86_64 23 | {{- else if eq .Arch "386" }}i386 24 | {{- else }}{{ .Arch }}{{ end }} 25 | {{- if .Arm }}v{{ .Arm }}{{ end }} 26 | 27 | format_overrides: 28 | - goos: windows 29 | format: zip 30 | 31 | checksum: 32 | name_template: 'checksums.txt' 33 | 34 | snapshot: 35 | version_template: "{{ incpatch .Version }}-next" 36 | 37 | changelog: 38 | sort: asc 39 | filters: 40 | exclude: 41 | - '^docs:' 42 | - '^test:' 43 | 44 | brews: 45 | - name: suimon 46 | homepage: "https://github.com/bartosian/homebrew-tools" 47 | url_template: "https://github.com/bartosian/suimon/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 48 | commit_author: 49 | name: bartosian 50 | email: official@bartestnet.com 51 | repository: 52 | owner: bartosian 53 | name: homebrew-tools 54 | branch: main 55 | token: "{{ .Env.GITHUB_TOKEN }}" 56 | 57 | release: 58 | github: 59 | owner: bartosian 60 | name: suimon -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleNodeTable.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl // temporary disabled 2 | package tablebuilder 3 | 4 | import ( 5 | "sort" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/enums" 8 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 9 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 10 | ) 11 | 12 | // handleNodeTable handles the configuration for the Node table. 13 | func (tb *Builder) handleNodeTable(hosts []domainhost.Host) error { 14 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeNode) 15 | 16 | sort.SliceStable(hosts, func(i, j int) bool { 17 | left, right := hosts[i], hosts[j] 18 | if left.Status != right.Status { 19 | return left.Status > right.Status 20 | } 21 | 22 | if left.Metrics.TotalTransactionsBlocks != right.Metrics.TotalTransactionsBlocks { 23 | return left.Metrics.TotalTransactionsBlocks > right.Metrics.TotalTransactionsBlocks 24 | } 25 | 26 | return left.Metrics.HighestSyncedCheckpoint > right.Metrics.HighestSyncedCheckpoint 27 | }) 28 | 29 | for idx := range hosts { 30 | host := hosts[idx] 31 | 32 | if !host.Metrics.Updated { 33 | continue 34 | } 35 | 36 | columnValues := tables.GetNodeColumnValues(idx, &host) 37 | 38 | tableConfig.Columns.SetColumnValues(columnValues) 39 | 40 | tableConfig.RowsCount++ 41 | } 42 | 43 | tb.config = tableConfig 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleValidatorTable.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl // temporary disabled 2 | package tablebuilder 3 | 4 | import ( 5 | "sort" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/enums" 8 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 9 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 10 | ) 11 | 12 | // handleValidatorTable handles the configuration for the Validator table. 13 | func (tb *Builder) handleValidatorTable(hosts []domainhost.Host) error { 14 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeValidator) 15 | 16 | sort.SliceStable(hosts, func(i, j int) bool { 17 | left, right := hosts[i], hosts[j] 18 | if left.Status != right.Status { 19 | return left.Status > right.Status 20 | } 21 | 22 | if left.Metrics.LastCommittedLeaderRound != right.Metrics.LastCommittedLeaderRound { 23 | return left.Metrics.LastCommittedLeaderRound > right.Metrics.LastCommittedLeaderRound 24 | } 25 | 26 | return left.Metrics.HighestSyncedCheckpoint > right.Metrics.HighestSyncedCheckpoint 27 | }) 28 | 29 | for idx := range hosts { 30 | host := hosts[idx] 31 | 32 | if !host.Metrics.Updated { 33 | continue 34 | } 35 | 36 | columnValues := tables.GetValidatorColumnValues(idx, &host) 37 | 38 | tableConfig.Columns.SetColumnValues(columnValues) 39 | 40 | tableConfig.RowsCount++ 41 | } 42 | 43 | tb.config = tableConfig 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/dynamic.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | "github.com/bartosian/suimon/internal/core/domain/service/dashboardbuilder" 8 | ) 9 | 10 | // Dynamic is a method of the Controller struct, responsible for initializing and rendering dashboards 11 | // based on the configuration data. 12 | func (c *Controller) Dynamic() error { 13 | // Parse the configuration data. 14 | if err := c.ParseConfigData(enums.MonitorTypeDynamic); err != nil { 15 | return err 16 | } 17 | 18 | // Initialize dashboard based on the configuration data. 19 | if err := c.InitDashboard(); err != nil { 20 | return err 21 | } 22 | 23 | // Render the dashboard and return error if any 24 | return c.RenderDashboards() 25 | } 26 | 27 | // InitDashboard initializes the enabled dashboard based on the display configuration. 28 | // It retrieves the corresponding hosts for the dashboard and initializes the dashboard builder. 29 | // If an error occurs during table initialization, it returns an error. 30 | func (c *Controller) InitDashboard() error { 31 | selectedDashboard := c.selectedDashboard 32 | 33 | host, err := c.selectHostForDashboard() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | builder, err := dashboardbuilder.NewBuilder(selectedDashboard, host, c.gateways.cli) 39 | if err != nil { 40 | return fmt.Errorf("error creating dashboard %s: %w", selectedDashboard, err) 41 | } 42 | 43 | c.builders.dynamic[selectedDashboard] = builder 44 | 45 | return builder.Init() 46 | } 47 | -------------------------------------------------------------------------------- /internal/core/handlers/commands/versionhandler.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl // temporary disabled 2 | package cmdhandlers 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/bartosian/suimon/internal/core/ports" 8 | ) 9 | 10 | type VersionHandler struct { 11 | command *cobra.Command 12 | controller ports.VersionController 13 | } 14 | 15 | func NewVersionHandler( 16 | controller ports.VersionController, 17 | ) *VersionHandler { 18 | handler := &VersionHandler{ 19 | controller: controller, 20 | } 21 | 22 | handler.command = handler.newCommand() 23 | 24 | return handler 25 | } 26 | 27 | func (h *VersionHandler) Start() { 28 | _ = h.command.Execute() 29 | } 30 | 31 | func (h *VersionHandler) AddSubCommands(subcommands ...ports.Command) { 32 | for _, subcommand := range subcommands { 33 | h.command.AddCommand(subcommand.Command()) 34 | } 35 | } 36 | 37 | func (h *VersionHandler) Command() *cobra.Command { 38 | return h.command 39 | } 40 | 41 | func (h *VersionHandler) newCommand() *cobra.Command { 42 | cmd := &cobra.Command{ 43 | Use: "version", 44 | Aliases: []string{"v"}, 45 | Short: "Show version information for the suimon monitoring tool", 46 | Long: "The suimon version subcommand displays the version information for the suimon monitoring tool. This includes the version number and build date. Use this command to quickly check the version of suimon that you are running.", 47 | Run: h.handleCommand, 48 | } 49 | 50 | return cmd 51 | } 52 | 53 | func (h *VersionHandler) handleCommand(_ *cobra.Command, _ []string) { 54 | h.controller.PrintVersion() 55 | } 56 | -------------------------------------------------------------------------------- /internal/core/handlers/commands/statichandler.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl // temporary disabled 2 | package cmdhandlers 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/bartosian/suimon/internal/core/ports" 8 | ) 9 | 10 | type StaticHandler struct { 11 | command *cobra.Command 12 | controller ports.VersionController 13 | } 14 | 15 | func NewStaticHandler( 16 | controller ports.VersionController, 17 | ) *StaticHandler { 18 | handler := &StaticHandler{ 19 | controller: controller, 20 | } 21 | 22 | handler.command = handler.newCommand() 23 | 24 | return handler 25 | } 26 | 27 | func (h *StaticHandler) Start() { 28 | _ = h.command.Execute() 29 | } 30 | 31 | func (h *StaticHandler) AddSubCommands(subcommands ...ports.Command) { 32 | for _, subcommand := range subcommands { 33 | h.command.AddCommand(subcommand.Command()) 34 | } 35 | } 36 | 37 | func (h *StaticHandler) Command() *cobra.Command { 38 | return h.command 39 | } 40 | 41 | func (h *StaticHandler) newCommand() *cobra.Command { 42 | cmd := &cobra.Command{ 43 | Use: "static", 44 | Aliases: []string{"s"}, 45 | Short: "Render static monitoring tables for the suimon monitoring tool", 46 | Long: "The suimon static subcommand renders static monitoring tables for the suimon monitoring tool. Use this command to view various statistics related to the running network, such as the number of validators, peers, and gas prices. You can select which tables to render using the command line interface.", 47 | Run: h.handleCommand, 48 | } 49 | 50 | return cmd 51 | } 52 | 53 | func (h *StaticHandler) handleCommand(_ *cobra.Command, _ []string) { 54 | h.controller.PrintVersion() 55 | } 56 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleValidatorAtRiskTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/enums" 8 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 9 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 10 | ) 11 | 12 | const base = 10 13 | 14 | // handleValidatorsAtRiskTable handles the configuration for the Validators At Risk table. 15 | // It takes the system state, extracts the necessary data, and updates the table configuration. 16 | func (tb *Builder) handleValidatorsAtRiskTable(systemState *domainmetrics.SuiSystemState) error { 17 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeValidatorsAtRisk) 18 | 19 | validatorsAtRisk := systemState.ValidatorsAtRiskParsed 20 | 21 | // Optimized sorting logic 22 | sort.SliceStable(validatorsAtRisk, func(i, j int) bool { 23 | leftEpochs, leftErr := strconv.ParseInt(validatorsAtRisk[i].EpochsAtRisk, base, 64) 24 | rightEpochs, rightErr := strconv.ParseInt(validatorsAtRisk[j].EpochsAtRisk, base, 64) 25 | 26 | if leftErr != nil || rightErr != nil { 27 | return leftErr == nil 28 | } 29 | 30 | if leftEpochs != rightEpochs { 31 | return leftEpochs > rightEpochs 32 | } 33 | 34 | return validatorsAtRisk[i].Name < validatorsAtRisk[j].Name 35 | }) 36 | 37 | for idx, validator := range validatorsAtRisk { 38 | columnValues := tables.GetValidatorAtRiskColumnValues(idx, validator) 39 | 40 | tableConfig.Columns.SetColumnValues(columnValues) 41 | 42 | tableConfig.RowsCount++ 43 | } 44 | 45 | tb.config = tableConfig 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/column.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "github.com/mum4k/termdash/container/grid" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | ) 8 | 9 | // ColumnsConfig is a type that maps column names to their respective widths. 10 | type ColumnsConfig map[enums.ColumnName]int 11 | 12 | // ColumnValues represents a mapping between column names and their corresponding values. 13 | type ColumnValues map[enums.ColumnName]any 14 | 15 | // ColumnOptions represents a mapping between column names and their corresponding configuration options. 16 | type ColumnOptions map[enums.ColumnName]any 17 | 18 | // Columns is a type that maps column names to their respective grid elements. 19 | type Columns map[enums.ColumnName]grid.Element 20 | 21 | // NewColumnFixed creates a new column element with a fixed width and a list of sub-elements. 22 | // The `width` parameter specifies the width of the column in pixels, and the `elements` parameter 23 | // is a list of sub-elements that will be contained within the column. 24 | func NewColumnFixed(width int, elements ...grid.Element) grid.Element { 25 | return grid.ColWidthFixed(width, elements...) 26 | } 27 | 28 | // NewColumnPct creates a new column element with a width proportional to the total grid width 29 | // and a list of sub-elements. The `width` parameter specifies the percentage of the grid width 30 | // that the column should occupy, and the `elements` parameter is a list of sub-elements that will 31 | // be contained within the column. 32 | func NewColumnPct(width int, elements ...grid.Element) grid.Element { 33 | return grid.ColWidthPerc(width, elements...) 34 | } 35 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/row.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "github.com/mum4k/termdash/container/grid" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | ) 8 | 9 | // RowsConfig is a type that represents the configuration for a set of rows in a grid. 10 | // Each `RowConfig` contains a height and a list of column names that should be included 11 | // in the row. 12 | type RowsConfig []RowConfig 13 | 14 | // RowConfig is a type that represents the configuration for a single row in a grid. 15 | // It contains a height and a list of column names that should be included in the row. 16 | type RowConfig struct { 17 | Columns []enums.ColumnName 18 | Height int 19 | } 20 | 21 | // Rows is a type that represents a set of grid rows, each of which is an element in the grid. 22 | type Rows []grid.Element 23 | 24 | // NewRowFixed creates a new row element with a fixed height and a list of sub-elements. 25 | // The `height` parameter specifies the height of the row in pixels, and the `elements` parameter 26 | // is a list of sub-elements that will be contained within the row. 27 | func NewRowFixed(height int, elements ...grid.Element) grid.Element { 28 | return grid.RowHeightFixed(height, elements...) 29 | } 30 | 31 | // NewRowPct creates a new row element with a height proportional to the total grid height 32 | // and a list of sub-elements. The `height` parameter specifies the percentage of the grid height 33 | // that the row should occupy, and the `elements` parameter is a list of sub-elements that will 34 | // be contained within the row. 35 | func NewRowPct(height int, elements ...grid.Element) grid.Element { 36 | return grid.RowHeightPerc(height, elements...) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/bartosian/suimon/internal/core/controllers" 8 | "github.com/bartosian/suimon/internal/core/controllers/monitor" 9 | domainconfig "github.com/bartosian/suimon/internal/core/domain/config" 10 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 11 | cmdhandlers "github.com/bartosian/suimon/internal/core/handlers/commands" 12 | ) 13 | 14 | func main() { 15 | cliGateway := cligw.NewGateway() 16 | 17 | defer handlePanic(cliGateway) 18 | 19 | config, err := domainconfig.NewConfig() 20 | if err != nil { 21 | // If an error occurs during initialization of the tables object, log the error and exit the program. 22 | cliGateway.Error(err.Error()) 23 | 24 | return 25 | } 26 | 27 | // Instantiate controllers 28 | rootController := controllers.NewRootController(cliGateway) 29 | versionController := controllers.NewVersionController(cliGateway) 30 | monitorController := monitor.NewController(config, cliGateway) 31 | 32 | // Instantiate Handlers - Root 33 | rootCmdHandler := cmdhandlers.NewRootHandler(rootController) 34 | 35 | // Instantiate Handlers - second level 36 | versionCmdHandler := cmdhandlers.NewVersionHandler(versionController) 37 | monitorCmdHandler := cmdhandlers.NewMonitorHandler(monitorController) 38 | 39 | // Add subcommands to the root command handler 40 | rootCmdHandler.AddSubCommands(versionCmdHandler, monitorCmdHandler) 41 | 42 | // Start the root command handler 43 | rootCmdHandler.Start() 44 | } 45 | 46 | func handlePanic(cliGateway *cligw.Gateway) { 47 | if r := recover(); r != nil { 48 | // Handle the panic by logging the error and exiting the program 49 | cliGateway.Error(fmt.Sprintf("panic: %v", r)) 50 | 51 | os.Exit(1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | 5 | linters: 6 | enable: 7 | - errcheck 8 | - gosimple 9 | - ineffassign 10 | - typecheck 11 | - goimports 12 | - govet 13 | - staticcheck 14 | - godot 15 | - misspell 16 | - whitespace 17 | - goconst 18 | - gocritic 19 | - nolintlint 20 | - nakedret 21 | - forbidigo 22 | - wsl 23 | - revive 24 | - errorlint 25 | 26 | issues: 27 | exclude-use-default: false 28 | max-issues-per-linter: 0 29 | max-same-issues: 0 30 | fix: true 31 | exclude-dirs: 32 | - packages/js/ 33 | - packages/go/proto/ 34 | - packages/go/db/ 35 | - node_modules 36 | 37 | linters-settings: 38 | errcheck: 39 | check-type-assertions: true 40 | goconst: 41 | min-len: 2 42 | min-occurrences: 3 43 | gocritic: 44 | enabled-tags: 45 | - diagnostic 46 | - experimental 47 | - opinionated 48 | - style 49 | govet: 50 | # Enable all analyzers. 51 | # Default: false 52 | enable-all: true 53 | # Disable analyzers by name. 54 | # Run `go tool vet help` to see all analyzers. 55 | # Default: [] 56 | disable: 57 | - fieldalignment # too strict 58 | # Settings per analyzer. 59 | settings: 60 | shadow: 61 | # Whether to be strict about shadowing; can be noisy. 62 | # Default: false 63 | strict: true 64 | nolintlint: 65 | require-explanation: true 66 | require-specific: true 67 | nakedret: 68 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 69 | max-func-lines: 16 70 | revive: 71 | rules: 72 | - name: package-comments 73 | severity: warning 74 | disabled: true 75 | output: 76 | sort-results: true 77 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/rendertables.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | ) 8 | 9 | // RenderTables renders the selected tables. The function checks whether data has been provided for each table 10 | // and enables or disables the table based on the availability of data. For each selected table, the function 11 | // retrieves the corresponding table builder from the static table builders map and calls its Render method. 12 | // The function returns nil if all selected tables have been rendered successfully. 13 | func (c *Controller) RenderTables() error { 14 | selectedTables := c.selectedTables 15 | 16 | rpcProvided := len(c.hosts.rpc) > 0 17 | nodeProvided := len(c.hosts.node) > 0 18 | validatorProvided := len(c.hosts.validator) > 0 19 | releasesProvided := len(c.releases) > 0 20 | 21 | tableTypeEnabled := map[enums.TableType]bool{ 22 | enums.TableTypeRPC: rpcProvided, 23 | enums.TableTypeNode: nodeProvided, 24 | enums.TableTypeValidator: validatorProvided, 25 | enums.TableTypeGasPriceAndSubsidy: rpcProvided, 26 | enums.TableTypeProtocol: rpcProvided, 27 | enums.TableTypeValidatorParams: rpcProvided, 28 | enums.TableTypeValidatorsAtRisk: rpcProvided, 29 | enums.TableTypeValidatorReports: rpcProvided, 30 | enums.TableTypeActiveValidators: rpcProvided, 31 | enums.TableTypeReleases: releasesProvided, 32 | } 33 | 34 | for _, tableType := range selectedTables { 35 | if !tableTypeEnabled[tableType] { 36 | continue 37 | } 38 | 39 | builder := c.builders.static[tableType] 40 | 41 | if err := builder.Render(); err != nil { 42 | return fmt.Errorf("error rendering table %s: %w", tableType, err) 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/core/domain/metrics/release.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | const releseAPIURL = "https://api.github.com/repos/MystenLabs/sui/releases?per_page=50" 13 | 14 | // Release represents a GitHub release. 15 | type Release struct { 16 | TagName string `json:"tag_name"` 17 | CommitHash string `json:"target_commitish"` 18 | Name string `json:"name"` 19 | PublishedAt string `json:"published_at"` 20 | CreatedAt string `json:"created_at"` 21 | URL string `json:"html_url"` 22 | Author struct { 23 | Login string `json:"login"` 24 | } `json:"author"` 25 | Draft bool `json:"draft"` 26 | PreRelease bool `json:"prerelease"` 27 | } 28 | 29 | // getReleases fetches releases for a given repo and filters them by network name. 30 | func GetReleases(networkName string) ([]Release, error) { 31 | resp, err := http.Get(releseAPIURL) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | defer func() { 37 | if closeErr := resp.Body.Close(); closeErr != nil { 38 | slog.Error("failed to close response body", "error", closeErr) 39 | } 40 | }() 41 | 42 | if resp.StatusCode != http.StatusOK { 43 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 44 | } 45 | 46 | body, err := io.ReadAll(resp.Body) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | var allReleases []Release 52 | 53 | err = json.Unmarshal(body, &allReleases) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | var filteredReleases []Release 59 | 60 | for _, release := range allReleases { 61 | if strings.HasPrefix(strings.ToLower(release.Name), strings.ToLower(networkName)) { 62 | filteredReleases = append(filteredReleases, release) 63 | } 64 | } 65 | 66 | return filteredReleases, nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/static.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | "github.com/bartosian/suimon/internal/core/domain/host" 8 | "github.com/bartosian/suimon/internal/core/domain/metrics" 9 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder" 10 | ) 11 | 12 | // Static is a method of the Controller struct, responsible for initializing and rendering tables 13 | // based on the configuration data. 14 | func (c *Controller) Static() error { 15 | // Parse the configuration data. 16 | if err := c.ParseConfigData(enums.MonitorTypeStatic); err != nil { 17 | return err 18 | } 19 | 20 | // Initialize tables based on the configuration data. 21 | if err := c.InitTables(); err != nil { 22 | return err 23 | } 24 | 25 | // Render the tables and return error if any 26 | return c.RenderTables() 27 | } 28 | 29 | // InitTables initializes the enabled tables based on the display configuration. 30 | // It retrieves the corresponding hosts for each table and initializes the table builder. 31 | // If an error occurs during table initialization, it returns an error. 32 | func (c *Controller) InitTables() error { 33 | for _, tableType := range c.selectedTables { 34 | var hosts []host.Host 35 | 36 | var releases []metrics.Release 37 | 38 | if tableType == enums.TableTypeReleases { 39 | releases = c.releases 40 | } else { 41 | hosts, _ = c.getHostsByTableType(tableType) 42 | } 43 | 44 | if len(hosts) == 0 && len(releases) == 0 { 45 | continue 46 | } 47 | 48 | builder := tablebuilder.NewBuilder(tableType, hosts, releases, c.gateways.cli) 49 | c.builders.static[tableType] = builder 50 | 51 | if err := builder.Init(); err != nil { 52 | return fmt.Errorf("error initializing table %s: %w", tableType, err) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/core/handlers/commands/monitorhandler.go: -------------------------------------------------------------------------------- 1 | package cmdhandlers 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/bartosian/suimon/internal/core/ports" 9 | ) 10 | 11 | type MonitorHandler struct { 12 | command *cobra.Command 13 | controller ports.MonitorController 14 | } 15 | 16 | func NewMonitorHandler( 17 | controller ports.MonitorController, 18 | ) *MonitorHandler { 19 | handler := &MonitorHandler{ 20 | controller: controller, 21 | } 22 | 23 | handler.command = handler.newCommand() 24 | 25 | return handler 26 | } 27 | 28 | func (h *MonitorHandler) Start() { 29 | _ = h.command.Execute() 30 | } 31 | 32 | func (h *MonitorHandler) AddSubCommands(subcommands ...ports.Command) { 33 | for _, subcommand := range subcommands { 34 | h.command.AddCommand(subcommand.Command()) 35 | } 36 | } 37 | 38 | func (h *MonitorHandler) Command() *cobra.Command { 39 | return h.command 40 | } 41 | 42 | func (h *MonitorHandler) newCommand() *cobra.Command { 43 | cmd := &cobra.Command{ 44 | Use: "monitor", 45 | Aliases: []string{"m"}, 46 | Short: "Monitor the running network with the suimon monitoring tool.", 47 | Long: "The suimon monitor subcommand allows you to monitor the running network with the suimon monitoring tool. This command provides options to render both static and dynamic dashboards. Static dashboards display various statistics related to the running network, such as the number of validators, peers, and gas prices. Dynamic dashboards provide real-time information about the network, such as block times and transaction throughput. You can select which dashboards to render using the command line interface. Use this command to keep an eye on the health and performance of your running network.", 48 | Run: h.handleCommand, 49 | } 50 | 51 | return cmd 52 | } 53 | 54 | func (h *MonitorHandler) handleCommand(_ *cobra.Command, _ []string) { 55 | if err := h.controller.Monitor(); err != nil { 56 | slog.Error("Failed to run", "error", err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/base.go: -------------------------------------------------------------------------------- 1 | package dashboardbuilder 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/mum4k/termdash/container" 9 | "github.com/mum4k/termdash/keyboard" 10 | "github.com/mum4k/termdash/terminal/termbox" 11 | "github.com/mum4k/termdash/terminal/terminalapi" 12 | 13 | "github.com/bartosian/suimon/internal/core/domain/enums" 14 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 15 | "github.com/bartosian/suimon/internal/core/domain/service/dashboardbuilder/dashboards" 16 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 17 | ) 18 | 19 | type Builder struct { 20 | ctx context.Context 21 | cliGateway *cligw.Gateway 22 | terminal *termbox.Terminal 23 | dashboard *container.Container 24 | host *domainhost.Host 25 | cells dashboards.Cells 26 | quitter func(k *terminalapi.Keyboard) 27 | tableType enums.TableType 28 | } 29 | 30 | // NewBuilder creates a new Builder instance with the provided CLI gateway. 31 | // It initializes the termbox terminal and dashboard, and sets up a context and quitter function. 32 | // If an error occurs during initialization, it returns an error. 33 | func NewBuilder(tableType enums.TableType, host *domainhost.Host, cliGateway *cligw.Gateway) (*Builder, error) { 34 | terminal, err := termbox.New() 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to initialize termbox terminal: %w", err) 37 | } 38 | 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | 41 | return &Builder{ 42 | ctx: ctx, 43 | tableType: tableType, 44 | cliGateway: cliGateway, 45 | terminal: terminal, 46 | host: host, 47 | quitter: func(k *terminalapi.Keyboard) { 48 | if k.Key == 'q' || k.Key == 'Q' || k.Key == keyboard.KeyEsc || k.Key == keyboard.KeyCtrlC { 49 | terminal.Close() 50 | cancel() 51 | 52 | os.Exit(0) 53 | } 54 | }, 55 | }, nil 56 | } 57 | 58 | // The tearDown function closes the Builder's terminal and cancels its context. 59 | func (db *Builder) tearDown() { 60 | db.ctx.Done() 61 | db.terminal.Close() 62 | } 63 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/rpc.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "github.com/mum4k/termdash/cell" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 8 | ) 9 | 10 | var ( 11 | ColumnsConfigRPC = ColumnsConfig{ 12 | enums.ColumnNameCurrentEpoch: ColumnWidth19, 13 | enums.ColumnNameSystemTimeTillNextEpoch: ColumnWidth19, 14 | enums.ColumnNameTotalTransactionBlocks: ColumnWidth30, 15 | enums.ColumnNameLatestCheckpoint: ColumnWidth30, 16 | } 17 | 18 | RowsConfigRPC = RowsConfig{ 19 | 0: { 20 | Height: RowHeight14, 21 | Columns: []enums.ColumnName{ 22 | enums.ColumnNameCurrentEpoch, 23 | enums.ColumnNameSystemTimeTillNextEpoch, 24 | enums.ColumnNameTotalTransactionBlocks, 25 | enums.ColumnNameLatestCheckpoint, 26 | }, 27 | }, 28 | } 29 | 30 | CellsConfigRPC = CellsConfig{ 31 | enums.ColumnNameCurrentEpoch: {"CURRENT EPOCH", cell.ColorGreen}, 32 | enums.ColumnNameSystemTimeTillNextEpoch: {"TIME TILL NEXT EPOCH", cell.ColorGreen}, 33 | enums.ColumnNameTotalTransactionBlocks: {"TOTAL TRANSACTION BLOCKS", cell.ColorYellow}, 34 | enums.ColumnNameLatestCheckpoint: {"LATEST CHECKPOINT", cell.ColorBlue}, 35 | } 36 | ) 37 | 38 | // GetRPCColumnValues returns a map of ColumnName values to corresponding values for a node at the specified index on the specified host. 39 | // The function retrieves information about the node from the host's internal state and formats it into a map of NodeColumnName keys and corresponding values. 40 | // The function also includes emoji values in the map if the specified flag is true. 41 | func GetRPCColumnValues(host *domainhost.Host) (ColumnValues, error) { 42 | return ColumnValues{ 43 | enums.ColumnNameTotalTransactionBlocks: host.Metrics.TotalTransactionsBlocks, 44 | enums.ColumnNameLatestCheckpoint: host.Metrics.LatestCheckpoint, 45 | enums.ColumnNameCurrentEpoch: host.Metrics.SystemState.Epoch, 46 | enums.ColumnNameSystemTimeTillNextEpoch: host.Metrics.DurationTillEpochEndHHMM, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/column.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "github.com/jedib0t/go-pretty/v6/table" 5 | "github.com/jedib0t/go-pretty/v6/text" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/enums" 8 | ) 9 | 10 | const ( 11 | TableNoData = "no data" 12 | EmptyValue = "" 13 | RPCPortDefault = "9000" 14 | ) 15 | 16 | type ( 17 | // ColumnsConfig represents a mapping between column names and their corresponding column configuration. 18 | ColumnsConfig map[enums.ColumnName]Column 19 | 20 | // ColumnValues represents a mapping between column names and their corresponding values. 21 | ColumnValues map[enums.ColumnName]any 22 | 23 | Column struct { 24 | Config *table.ColumnConfig 25 | Values []any 26 | } 27 | ) 28 | 29 | // NewDefaultColumnConfig creates a new column configuration with the given alignment settings and hidden flag. 30 | func NewDefaultColumnConfig(alignHeader, align text.Align, hidden bool) Column { 31 | return Column{ 32 | Config: &table.ColumnConfig{ 33 | Align: align, 34 | AlignHeader: alignHeader, 35 | VAlign: text.VAlignMiddle, 36 | VAlignHeader: text.VAlignMiddle, 37 | Hidden: hidden, 38 | }, 39 | } 40 | } 41 | 42 | // SetValue sets the value of the column to the given value, appending it to the existing values slice. 43 | func (col *Column) SetValue(value any) { 44 | if value == nil || value == "" { 45 | value = TableNoData 46 | } 47 | 48 | col.Values = append(col.Values, value) 49 | } 50 | 51 | // SetNoDataValue creates a new copy of the column with a nil value set. 52 | func (col *Column) SetNoDataValue() *Column { 53 | newColumn := *col 54 | newColumn.SetValue(nil) 55 | 56 | return &newColumn 57 | } 58 | 59 | // SetNoDataValue creates a new slice of columns with a nil value set for each column. 60 | func (cols ColumnsConfig) SetNoDataValue() ColumnsConfig { 61 | newCols := make(ColumnsConfig, len(cols)) 62 | for idx, col := range cols { 63 | newCols[idx] = *col.SetNoDataValue() 64 | } 65 | 66 | return newCols 67 | } 68 | 69 | // SetColumnValues creates a new slice of columns with the given values set for each column at the corresponding index. 70 | func (cols ColumnsConfig) SetColumnValues(values ColumnValues) { 71 | for idx, col := range cols { 72 | newCol := col 73 | 74 | if value, ok := values[idx]; ok { 75 | newCol.SetValue(value) 76 | } 77 | 78 | cols[idx] = newCol 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/core/domain/enums/prometheusmetricname.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type PrometheusMetricName string 4 | 5 | const ( 6 | PrometheusMetricNameTotalTransactionCertificates PrometheusMetricName = "total_transaction_certificates" 7 | PrometheusMetricNameTotalTransactionEffects PrometheusMetricName = "total_transaction_effects" 8 | PrometheusMetricNameHighestKnownCheckpoint PrometheusMetricName = "highest_known_checkpoint" 9 | PrometheusMetricNameHighestSyncedCheckpoint PrometheusMetricName = "highest_synced_checkpoint" 10 | PrometheusMetricNameLastExecutedCheckpoint PrometheusMetricName = "last_executed_checkpoint" 11 | PrometheusMetricNameCurrentEpoch PrometheusMetricName = "current_epoch" 12 | PrometheusMetricNameEpochTotalDuration PrometheusMetricName = "epoch_total_duration" 13 | PrometheusMetricNameConsensusRoundProberCurrentRoundGaps PrometheusMetricName = "consensus_round_prober_current_round_gaps" 14 | PrometheusMetricNameSuiNetworkPeers PrometheusMetricName = "sui_network_peers" 15 | PrometheusMetricNameSkippedConsensusTransactions PrometheusMetricName = "skipped_consensus_txns" 16 | PrometheusMetricNameTotalSignatureErrors PrometheusMetricName = "total_signature_errors" 17 | PrometheusMetricNameTotalTransactionCertificatesCreated PrometheusMetricName = "total_tx_certificates_created" 18 | PrometheusMetricNameNonConsensusLatencySum PrometheusMetricName = "validator_service_handle_certificate_non_consensus_latency_sum" 19 | PrometheusMetricNameUptime PrometheusMetricName = "uptime" 20 | PrometheusMetricNameCurrentVotingRight PrometheusMetricName = "current_voting_right" 21 | PrometheusMetricNameNumberSharedObjectTransactions PrometheusMetricName = "num_shared_obj_tx" 22 | PrometheusMetricNameConsensusLastCommittedLeaderRound PrometheusMetricName = "consensus_last_committed_leader_round" 23 | PrometheusMetricNameConsensusCommittedMessages PrometheusMetricName = "consensus_committed_messages" 24 | PrometheusMetricNameConsensusProposedBlocks PrometheusMetricName = "consensus_proposed_blocks" 25 | PrometheusMetricNameConsensusHighestAcceptedRound PrometheusMetricName = "consensus_highest_accepted_round" 26 | ) 27 | 28 | func (e PrometheusMetricName) ToString() string { 29 | return string(e) 30 | } 31 | -------------------------------------------------------------------------------- /internal/pkg/progress/progress.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | progressbar "github.com/schollz/progressbar/v3" 9 | ) 10 | 11 | type Color string 12 | 13 | const ( 14 | ColorReset Color = "[reset]" 15 | ColorWhite Color = "[white]" 16 | ColorBlue Color = "[blue]" 17 | ColorRed Color = "[red]" 18 | ColorGreen Color = "[green]" 19 | ) 20 | 21 | const progressInterval = 100 22 | const progressMaxWidth = 1000 23 | const progresWidth = 30 24 | const progressTickerSleepInterval = 15 25 | 26 | // NewProgressBar creates a new progress bar with the specified action and color. 27 | // It takes the action string and color as input and returns a channel for controlling the progress bar. 28 | // The progress bar is updated at regular intervals and can be stopped by closing the returned channel. 29 | // The color parameter specifies the color of the progress bar. 30 | // Example usage: 31 | // 32 | // progressChan := NewProgressBar("Downloading", ColorBlue) 33 | // // Perform download operation 34 | // close(progressChan) // Stop the progress bar 35 | // 36 | // Note: It is important to close the returned channel to stop the progress bar and free resources. 37 | func NewProgressBar(action string, color Color) chan<- struct{} { 38 | progressTicker := time.NewTicker(progressInterval * time.Millisecond) 39 | progressChan := make(chan struct{}) 40 | 41 | bar := progressbar.NewOptions(progressMaxWidth, 42 | progressbar.OptionEnableColorCodes(true), 43 | progressbar.OptionSetElapsedTime(false), 44 | progressbar.OptionShowBytes(false), 45 | progressbar.OptionClearOnFinish(), 46 | progressbar.OptionSetWidth(progresWidth), 47 | progressbar.OptionSetDescription(fmt.Sprintf("%s [ %s... ] [reset]", color, action)), 48 | progressbar.OptionSetTheme(progressbar.Theme{ 49 | Saucer: "=", 50 | SaucerHead: ">", 51 | SaucerPadding: " ", 52 | BarStart: "[", 53 | BarEnd: "]", 54 | })) 55 | 56 | go func() { 57 | defer progressTicker.Stop() 58 | 59 | for { 60 | select { 61 | case <-progressChan: 62 | progressTicker.Stop() 63 | 64 | if err := bar.Clear(); err != nil { 65 | os.Exit(1) 66 | } 67 | 68 | return 69 | case <-progressTicker.C: 70 | for i := 0; i < 100; i++ { 71 | if err := bar.Add(1); err != nil { 72 | os.Exit(1) 73 | } 74 | 75 | time.Sleep(progressTickerSleepInterval * time.Millisecond) 76 | } 77 | } 78 | } 79 | }() 80 | 81 | return progressChan 82 | } 83 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/handleActiveValidatorsTable.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 10 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 11 | ) 12 | 13 | // handleActiveValidatorsTable handles the configuration for the Active Validators table. 14 | // It takes the system state, extracts the necessary data, and updates the table configuration. 15 | func (tb *Builder) handleActiveValidatorsTable(metrics *domainmetrics.Metrics) error { 16 | tableConfig := tables.NewDefaultTableConfig(enums.TableTypeActiveValidators) 17 | 18 | activeValidators := metrics.SystemState.ActiveValidators 19 | validatorsApy := metrics.ValidatorsApyParsed 20 | 21 | const base = 10 22 | 23 | sort.SliceStable(activeValidators, func(i, j int) bool { 24 | leftVotingPower, leftErr := strconv.ParseInt(activeValidators[i].VotingPower, base, 64) 25 | rightVotingPower, rightErr := strconv.ParseInt(activeValidators[j].VotingPower, base, 64) 26 | 27 | if leftErr != nil { 28 | return false 29 | } 30 | 31 | if rightErr != nil { 32 | return true 33 | } 34 | 35 | leftNextEpochStake, leftStakeErr := strconv.ParseInt(activeValidators[i].NextEpochStake, base, 64) 36 | rightNextEpochStake, rightStakeErr := strconv.ParseInt(activeValidators[j].NextEpochStake, base, 64) 37 | 38 | if leftStakeErr != nil { 39 | return false 40 | } 41 | 42 | if rightStakeErr != nil { 43 | return true 44 | } 45 | 46 | if leftVotingPower != rightVotingPower { 47 | return leftVotingPower > rightVotingPower 48 | } 49 | 50 | if leftNextEpochStake != rightNextEpochStake { 51 | return leftNextEpochStake > rightNextEpochStake 52 | } 53 | 54 | return activeValidators[i].Name < activeValidators[j].Name 55 | }) 56 | 57 | for idx, validator := range activeValidators { 58 | validatorApy, ok := validatorsApy[validator.SuiAddress] 59 | if !ok { 60 | return fmt.Errorf("failed to lookup validator APY by address: %s", validator.SuiAddress) 61 | } 62 | 63 | validator.APY = strconv.FormatFloat(validatorApy*100, 'f', 3, 64) 64 | 65 | columnValues, err := tables.GetActiveValidatorColumnValues(idx, validator) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | tableConfig.Columns.SetColumnValues(columnValues) 71 | 72 | tableConfig.RowsCount++ 73 | } 74 | 75 | tb.config = tableConfig 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/rpc.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "github.com/jedib0t/go-pretty/v6/text" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 8 | ) 9 | 10 | var ( 11 | ColumnsConfigRPC = ColumnsConfig{ 12 | enums.ColumnNameIndex: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 13 | enums.ColumnNameHealth: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 14 | enums.ColumnNameAddress: NewDefaultColumnConfig(text.AlignLeft, text.AlignCenter, false), 15 | enums.ColumnNamePortRPC: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 16 | enums.ColumnNameTotalTransactionBlocks: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 17 | enums.ColumnNameLatestCheckpoint: NewDefaultColumnConfig(text.AlignLeft, text.AlignLeft, false), 18 | enums.ColumnNameCurrentEpoch: NewDefaultColumnConfig(text.AlignLeft, text.AlignLeft, false), 19 | } 20 | RowsConfigRPC = RowsConfig{ 21 | 0: { 22 | enums.ColumnNameIndex, 23 | enums.ColumnNameHealth, 24 | enums.ColumnNameAddress, 25 | enums.ColumnNamePortRPC, 26 | enums.ColumnNameTotalTransactionBlocks, 27 | enums.ColumnNameLatestCheckpoint, 28 | enums.ColumnNameCurrentEpoch, 29 | }, 30 | } 31 | ) 32 | 33 | // GetRPCColumnValues returns a map of NodeColumnName values to corresponding values for the RPC service on the specified host. 34 | // The function retrieves information about the RPC service from the host's internal state and formats it into a map of NodeColumnName keys and corresponding values. 35 | // Returns a map of NodeColumnName keys to corresponding values. 36 | func GetRPCColumnValues(idx int, host *domainhost.Host) ColumnValues { 37 | status := host.Status.StatusToPlaceholder() 38 | 39 | port := host.Ports[enums.PortTypeRPC] 40 | if port == "" { 41 | port = RPCPortDefault 42 | } 43 | 44 | address := host.Endpoint.Address 45 | 46 | return ColumnValues{ 47 | enums.ColumnNameIndex: idx + 1, 48 | enums.ColumnNameHealth: status, 49 | enums.ColumnNameAddress: address, 50 | enums.ColumnNamePortRPC: port, 51 | enums.ColumnNameTotalTransactionBlocks: host.Metrics.TotalTransactionsBlocks, 52 | enums.ColumnNameLatestCheckpoint: host.Metrics.LatestCheckpoint, 53 | enums.ColumnNameCurrentEpoch: host.Metrics.SystemState.Epoch, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/release.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "github.com/jedib0t/go-pretty/v6/text" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainrelease "github.com/bartosian/suimon/internal/core/domain/metrics" 8 | ) 9 | 10 | var ( 11 | ColumnsConfigRelease = ColumnsConfig{ 12 | enums.ColumnNameIndex: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 13 | enums.ColumnNameReleaseName: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 14 | enums.ColumnNameDraft: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 15 | enums.ColumnNameCommit: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 16 | enums.ColumnNamePublishedAt: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 17 | enums.ColumnNameAuthor: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 18 | enums.ColumnNamePreRelease: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 19 | enums.ColumnNameURL: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 20 | enums.ColumnNameCreatedAt: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 21 | } 22 | RowsRelease = RowsConfig{ 23 | 0: { 24 | enums.ColumnNameIndex, 25 | enums.ColumnNameReleaseName, 26 | enums.ColumnNameDraft, 27 | enums.ColumnNamePreRelease, 28 | enums.ColumnNameCommit, 29 | enums.ColumnNameURL, 30 | enums.ColumnNameAuthor, 31 | enums.ColumnNameCreatedAt, 32 | enums.ColumnNamePublishedAt, 33 | }, 34 | } 35 | ) 36 | 37 | // GetReleaseColumnValues returns a map of ColumnName keys to corresponding values for the specified release. 38 | // The function retrieves information about the release from the provided metrics.Release object and formats it into a map of ColumnName keys and corresponding values. 39 | // Returns a map of ColumnName keys to corresponding values. 40 | func GetReleaseColumnValues(idx int, release *domainrelease.Release) ColumnValues { 41 | return ColumnValues{ 42 | enums.ColumnNameIndex: idx + 1, 43 | enums.ColumnNameReleaseName: release.Name, 44 | enums.ColumnNameTagName: release.TagName, 45 | enums.ColumnNameCommit: release.CommitHash, 46 | enums.ColumnNameAuthor: release.Author.Login, 47 | enums.ColumnNamePublishedAt: release.PublishedAt, 48 | enums.ColumnNameDraft: release.Draft, 49 | enums.ColumnNamePreRelease: release.PreRelease, 50 | enums.ColumnNameURL: release.URL, 51 | enums.ColumnNameCreatedAt: release.CreatedAt, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/core/domain/host/addressinfo.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | "github.com/bartosian/suimon/internal/pkg/address" 10 | ) 11 | 12 | const ( 13 | protocolHTTP = "http" 14 | protocolHTTPS = "https" 15 | 16 | rpcPortDefault = "9000" 17 | metricsPortDefault = "9184" 18 | 19 | metricsPathDefault = "/metrics" 20 | ) 21 | 22 | type AddressInfo struct { 23 | Ports map[enums.PortType]string 24 | Endpoint address.Endpoint 25 | } 26 | 27 | // GetUrlRPC generates a URL for the RPC endpoint of the address. 28 | // It constructs the URL using the protocol, host, port, and path 29 | // components of the endpoint, as well as the default port value. 30 | func (addr *AddressInfo) GetURLRPC() (string, error) { 31 | endpoint := addr.Endpoint 32 | ports := addr.Ports 33 | protocol := getProtocol(endpoint.SSL) 34 | 35 | hostURL, err := url.Parse(fmt.Sprintf("%s://", protocol)) 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | if endpoint.Host != nil { 41 | hostURL.Host = *endpoint.Host 42 | } else { 43 | hostURL.Host = *endpoint.IP 44 | } 45 | 46 | if rpcPort, ok := ports[enums.PortTypeRPC]; ok { 47 | hostURL.Host = net.JoinHostPort(hostURL.Host, rpcPort) 48 | } else if endpoint.IP != nil { 49 | hostURL.Host = net.JoinHostPort(*endpoint.IP, rpcPortDefault) 50 | } 51 | 52 | if endpoint.Path != nil { 53 | hostURL.Path = *endpoint.Path 54 | } 55 | 56 | return hostURL.String(), nil 57 | } 58 | 59 | // GetUrlPrometheus generates a URL for the Prometheus endpoint of the address. 60 | // It constructs the URL using the protocol, host, port, and path 61 | // components of the endpoint, as well as the default port and path values. 62 | func (addr *AddressInfo) GetURLPrometheus() (string, error) { 63 | endpoint := addr.Endpoint 64 | ports := addr.Ports 65 | protocol := getProtocol(endpoint.SSL) 66 | 67 | hostURL, err := url.Parse(fmt.Sprintf("%s://", protocol)) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | if endpoint.Host != nil { 73 | hostURL.Host = *endpoint.Host 74 | } else { 75 | hostURL.Host = *endpoint.IP 76 | } 77 | 78 | if metricsPort, ok := ports[enums.PortTypeMetrics]; ok { 79 | hostURL.Host = net.JoinHostPort(hostURL.Host, metricsPort) 80 | } else if endpoint.IP != nil { 81 | hostURL.Host = net.JoinHostPort(*endpoint.IP, metricsPortDefault) 82 | } 83 | 84 | hostURL.Path = metricsPathDefault 85 | 86 | return hostURL.String(), nil 87 | } 88 | 89 | // getProtocol returns the protocol based on the secure flag. 90 | func getProtocol(secure bool) string { 91 | protocol := protocolHTTP 92 | if secure { 93 | protocol = protocolHTTPS 94 | } 95 | 96 | return protocol 97 | } 98 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/init.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 8 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 9 | ) 10 | 11 | // Init initializes the table configuration based on the given table type and host data. 12 | // It processes the host data and calls the appropriate handler function for the specified table type. 13 | func (tb *Builder) Init() error { 14 | if len(tb.hosts) == 0 && tb.tableType != enums.TableTypeReleases { 15 | return errors.New("hosts are not initialized") 16 | } 17 | 18 | handlerMap := map[enums.TableType]func() error{ 19 | enums.TableTypeNode: func() error { return tb.handleNodeTable(tb.hosts) }, 20 | enums.TableTypeRPC: func() error { return tb.handleRPCTable(tb.hosts) }, 21 | enums.TableTypeValidator: func() error { return tb.handleValidatorTable(tb.hosts) }, 22 | enums.TableTypeGasPriceAndSubsidy: func() error { return tb.handleTableWithMetrics(tb.hosts, tb.handleSystemStateTable) }, 23 | enums.TableTypeProtocol: func() error { return tb.handleTableWithMetrics(tb.hosts, tb.handleProtocolTable) }, 24 | enums.TableTypeValidatorParams: func() error { return tb.handleTableWithSystemState(tb.hosts, tb.handleValidatorParamsTable) }, 25 | enums.TableTypeValidatorsAtRisk: func() error { return tb.handleTableWithSystemState(tb.hosts, tb.handleValidatorsAtRiskTable) }, 26 | enums.TableTypeValidatorReports: func() error { return tb.handleTableWithSystemState(tb.hosts, tb.handleValidatorReportsTable) }, 27 | enums.TableTypeActiveValidators: func() error { return tb.handleTableWithMetrics(tb.hosts, tb.handleActiveValidatorsTable) }, 28 | enums.TableTypeReleases: func() error { return tb.handleReleasesTable(tb.Releases) }, 29 | } 30 | 31 | if handler, ok := handlerMap[tb.tableType]; ok { 32 | return handler() 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // handleTableWithMetrics is a generic wrapper for handling tables that require metrics. 39 | func (tb *Builder) handleTableWithMetrics(hosts []domainhost.Host, handlerFunc func(*domainmetrics.Metrics) error) error { 40 | if len(hosts) == 0 { 41 | return errors.New("no hosts available") 42 | } 43 | 44 | return handlerFunc(&hosts[0].Metrics) 45 | } 46 | 47 | // handleTableWithSystemState is a generic wrapper for handling tables that require system state. 48 | func (tb *Builder) handleTableWithSystemState(hosts []domainhost.Host, handlerFunc func(*domainmetrics.SuiSystemState) error) error { 49 | if len(hosts) == 0 { 50 | return errors.New("no hosts available") 51 | } 52 | 53 | return handlerFunc(&hosts[0].Metrics.SystemState) 54 | } 55 | -------------------------------------------------------------------------------- /internal/core/domain/config/base.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | const ( 13 | suimonConfigEnvVar = "SUIMON_CONFIG_PATH" 14 | suimonConfigDir = ".suimon" 15 | ) 16 | 17 | type Config struct { 18 | IPLookup struct { 19 | AccessToken string `yaml:"access-token"` 20 | } `yaml:"ip-lookup"` 21 | ReferenceRPC []string `yaml:"reference-rpc"` 22 | FullNodes []struct { 23 | JSONRPCAddress string `yaml:"json-rpc-address"` 24 | MetricsAddress string `yaml:"metrics-address"` 25 | } `yaml:"full-nodes"` 26 | Validators []struct { 27 | MetricsAddress string `yaml:"metrics-address"` 28 | } `yaml:"validators"` 29 | } 30 | 31 | // NewConfig reads the Suimon configuration files from the directory specified by 32 | // the SUIMON_CONFIG_PATH environment variable or the default directory if the 33 | // environment variable is not set, and returns a map of Config objects with the 34 | // file name segments as the keys. 35 | func NewConfig() (map[string]Config, error) { 36 | dirPath := os.Getenv(suimonConfigEnvVar) 37 | if dirPath == "" { 38 | homeDir, err := os.UserHomeDir() 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | dirPath = filepath.Join(homeDir, suimonConfigDir) 44 | } 45 | 46 | return readConfigs(dirPath) 47 | } 48 | 49 | // readConfigs reads the Suimon configuration files from the specified directory, 50 | // creates a map of Config objects with the file name segments as the keys, and returns 51 | // the map. The file name segments are converted to uppercase before being used as keys. 52 | func readConfigs(dirPath string) (map[string]Config, error) { 53 | configs := make(map[string]Config) 54 | 55 | // Retrieve .yml files 56 | ymlFiles, err := filepath.Glob(filepath.Join(dirPath, "*.yml")) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | // Retrieve .yaml files 62 | yamlFiles, err := filepath.Glob(filepath.Join(dirPath, "*.yaml")) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // Combine file lists 68 | ymlFiles = append(ymlFiles, yamlFiles...) 69 | 70 | if len(ymlFiles) == 0 { 71 | return nil, fmt.Errorf("no Suimon configuration files found in %s", dirPath) 72 | } 73 | 74 | for _, file := range ymlFiles { 75 | fileData, readErr := os.ReadFile(file) 76 | if readErr != nil { 77 | return nil, fmt.Errorf("error reading file %s: %w", file, readErr) 78 | } 79 | 80 | var config Config 81 | if unmarshalErr := yaml.Unmarshal(fileData, &config); unmarshalErr != nil { 82 | return nil, fmt.Errorf("error unmarshaling YAML in file %s: %w", file, unmarshalErr) 83 | } 84 | 85 | filename := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) 86 | filename = strings.TrimPrefix(filename, "suimon-") 87 | filename = strings.ToUpper(filename) 88 | 89 | configs[filename] = config 90 | } 91 | 92 | return configs, nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/core/domain/enums/metrictype.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | type MetricType string 4 | 5 | const ( 6 | MetricTypeSuiSystemState MetricType = "SYSTEM_STATE" 7 | MetricTypeValidatorsApy MetricType = "VALIDATORS_APY" 8 | MetricTypeTotalTransactionBlocks MetricType = "TOTAL_TRANSACTION_BLOCKS" 9 | MetricTypeTotalTransactionCertificates MetricType = "TOTAL_TRANSACTION_CERTIFICATES" 10 | MetricTypeTotalTransactionEffects MetricType = "TOTAL_TRANSACTION_EFFECTS" 11 | MetricTypeTransactionsPerSecond MetricType = "TRANSACTIONS_PER_SECOND" 12 | MetricTypeLatestCheckpoint MetricType = "LATEST_CHECKPOINT" 13 | MetricTypeHighestKnownCheckpoint MetricType = "HIGHEST_KNOWN_CHECKPOINT" 14 | MetricTypeHighestSyncedCheckpoint MetricType = "HIGHEST_SYNCED_CHECKPOINT" 15 | MetricTypeLastExecutedCheckpoint MetricType = "LAST_EXECUTED_CHECKPOINT" 16 | MetricTypeCheckpointExecBacklog MetricType = "CHECKPOINT_EXECUTION_BACKLOG" 17 | MetricTypeCheckpointSyncBacklog MetricType = "CHECKPOINT_SYNC_BACKLOG" 18 | MetricTypeCheckpointsPerSecond MetricType = "CHECKPOINTS_PER_SECOND" 19 | MetricTypeCurrentEpoch MetricType = "CURRENT_EPOCH" 20 | MetricTypeEpochTotalDuration MetricType = "EPOCH_TOTAL_DURATION" 21 | MetricTypeTimeTillNextEpoch MetricType = "TIME_TILL_NEXT_EPOCH" 22 | MetricTypeTxSyncPercentage MetricType = "TX_SYNC_PERCENTAGE" 23 | MetricTypeCheckSyncPercentage MetricType = "CHECK_SYNC_PERCENTAGE" 24 | MetricTypeSuiNetworkPeers MetricType = "SUI_NETWORK_PEERS" 25 | MetricTypeUptime MetricType = "UPTIME" 26 | MetricTypeVersion MetricType = "VERSION" 27 | MetricTypeCommit MetricType = "COMMIT" 28 | MetricTypeConsensusRoundProberCurrentRoundGaps MetricType = "CONSENSUS_ROUND_PROBER_CURRENT_ROUND_GAPS" 29 | MetricTypePrimaryNetworkPeers MetricType = "PRIMARY_NETWORK_PEERS" 30 | MetricTypeWorkerNetworkPeers MetricType = "WORKER_NETWORK_PEERS" 31 | MetricTypeSkippedConsensusTransactions MetricType = "SKIPPED_CONSENSUS_TRANSACTIONS" 32 | MetricTypeTotalSignatureErrors MetricType = "TOTAL_SIGNATURE_ERRORS" 33 | MetricTypeTotalTransactionCertificatesCreated MetricType = "TOTAL_TRANSACTION_CERTIFICATES_CREATED" 34 | MetricTypeNonConsensusLatencySum MetricType = "NON_CONSENSUS_LATENCY_SUM" 35 | MetricTypeProtocol MetricType = "PROTOCOL" 36 | MetricTypeCurrentVotingRight MetricType = "CURRENT_VOTING_RIGHT" 37 | MetricTypeConsensusLastCommittedLeaderRound MetricType = "CONSENSUS_LAST_COMMITTED_LEADER_ROUND" 38 | MetricTypeConsensusHighestAcceptedRound MetricType = "CONSENSUS_HIGHEST_ACCEPTED_ROUND" 39 | MetricTypeNumberSharedObjectTransactions MetricType = "NUMBER_OF_SHARED_OBJECT_TRANSACTIONS" 40 | ) 41 | 42 | func (e MetricType) ToString() string { 43 | return string(e) 44 | } 45 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/createhosts.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/hashicorp/go-multierror" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | "github.com/bartosian/suimon/internal/core/domain/host" 10 | "github.com/bartosian/suimon/internal/core/gateways/geogw" 11 | "github.com/bartosian/suimon/internal/core/gateways/prometheusgw" 12 | "github.com/bartosian/suimon/internal/core/gateways/rpcgw" 13 | ) 14 | 15 | type responseWithError struct { 16 | response *host.Host 17 | err error 18 | } 19 | 20 | // createHosts creates hosts based on the provided table type and addresses. 21 | // It initializes the hosts, processes the addresses, and sets up the necessary gateways for each host. 22 | // It returns the created hosts and any error encountered during the process. 23 | func (c *Controller) createHosts(table enums.TableType, addresses []host.AddressInfo) ([]host.Host, error) { 24 | hosts := make([]host.Host, 0, len(addresses)) 25 | processedAddresses := make(map[string]struct{}) 26 | 27 | respChan := make(chan responseWithError, len(addresses)) 28 | 29 | var wg sync.WaitGroup 30 | 31 | sendErrorResponse := func(result responseWithError, err error) { 32 | result.err = err 33 | respChan <- result 34 | } 35 | 36 | for _, addressInfo := range addresses { 37 | address := addressInfo.Endpoint.Address 38 | if _, ok := processedAddresses[address]; ok { 39 | continue 40 | } 41 | 42 | processedAddresses[address] = struct{}{} 43 | 44 | wg.Add(1) 45 | 46 | go func(addressInfo host.AddressInfo) { 47 | defer wg.Done() 48 | 49 | var result responseWithError 50 | 51 | rpcURL, err := addressInfo.GetURLRPC() 52 | if err != nil { 53 | sendErrorResponse(result, err) 54 | return 55 | } 56 | 57 | metricsURL, err := addressInfo.GetURLPrometheus() 58 | if err != nil { 59 | sendErrorResponse(result, err) 60 | return 61 | } 62 | 63 | rpcGateway := rpcgw.NewGateway(c.gateways.cli, rpcURL) 64 | prometheusGateway := prometheusgw.NewGateway(c.gateways.cli, metricsURL) 65 | geoGateway := geogw.NewGateway(c.gateways.cli, c.selectedConfig.IPLookup.AccessToken) 66 | 67 | createdHost := host.NewHost(table, addressInfo, rpcGateway, geoGateway, prometheusGateway, c.gateways.cli) 68 | result.response = createdHost 69 | 70 | if c.selectedConfig.IPLookup.AccessToken != "" { 71 | if createErr := createdHost.SetIPInfo(); createErr != nil { 72 | sendErrorResponse(result, createErr) 73 | return 74 | } 75 | } 76 | 77 | if getMetricsErr := createdHost.GetMetrics(); getMetricsErr != nil { 78 | sendErrorResponse(result, err) 79 | return 80 | } 81 | 82 | respChan <- result 83 | }(addressInfo) 84 | } 85 | 86 | go func() { 87 | wg.Wait() 88 | close(respChan) 89 | }() 90 | 91 | var mErr *multierror.Error 92 | 93 | for result := range respChan { 94 | if result.err != nil { 95 | mErr = multierror.Append(mErr, result.err) 96 | } else { 97 | hosts = append(hosts, *result.response) 98 | } 99 | } 100 | 101 | if len(hosts) == 0 { 102 | return nil, mErr.ErrorOrNil() 103 | } 104 | 105 | return hosts, nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/init.go: -------------------------------------------------------------------------------- 1 | package dashboardbuilder 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mum4k/termdash/container" 8 | "github.com/mum4k/termdash/container/grid" 9 | 10 | "github.com/bartosian/suimon/internal/core/domain/service/dashboardbuilder/dashboards" 11 | ) 12 | 13 | // Init initializes the dashboard by fetching and configuring the grid. 14 | func (db *Builder) Init() (err error) { 15 | defer func() { 16 | if err != nil { 17 | db.tearDown() 18 | } 19 | 20 | if recoverErr := recover(); recoverErr != nil { 21 | db.tearDown() 22 | db.cliGateway.Error(fmt.Sprintf("panic: %v", recoverErr)) 23 | os.Exit(1) 24 | } 25 | }() 26 | 27 | if setupErr := db.setupDashboard(); setupErr != nil { 28 | return setupErr 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // setupDashboard is the main method that orchestrates the dashboard setup. 35 | func (db *Builder) setupDashboard() error { 36 | cells, err := db.loadCells() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | db.cells = cells 42 | 43 | columns, err := db.loadColumns(cells) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | rows, err := db.loadRows(columns) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | builder := grid.New() 54 | builder.Add(rows...) 55 | 56 | options, err := builder.Build() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return db.createDashboard(options) 62 | } 63 | 64 | // loadCells fetches the cells configuration and builds the cells. 65 | func (db *Builder) loadCells() (dashboards.Cells, error) { 66 | cellsConfig, err := dashboards.GetCellsConfig(db.tableType) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | cells, err := dashboards.GetCells(cellsConfig) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return cells, nil 77 | } 78 | 79 | // loadColumns fetches the columns configuration and builds the columns based on the cells. 80 | func (db *Builder) loadColumns(cells dashboards.Cells) (dashboards.Columns, error) { 81 | columnsConfig, err := dashboards.GetColumnsConfig(db.tableType) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | columns, err := dashboards.GetColumns(columnsConfig, cells) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return columns, nil 92 | } 93 | 94 | // loadRows fetches the rows configuration and builds the rows based on the columns. 95 | func (db *Builder) loadRows(columns dashboards.Columns) ([]grid.Element, error) { 96 | rowsConfig, err := dashboards.GetRowsConfig(db.tableType) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | rows, err := dashboards.GetRows(rowsConfig, columns) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return rows, nil 107 | } 108 | 109 | // createDashboard creates the dashboard using the built grid and terminal. 110 | func (db *Builder) createDashboard(options []container.Option) error { 111 | dashboardConfig := append([]container.Option{}, dashboards.DashboardConfigDefault...) 112 | dashboardConfig = append(dashboardConfig, options...) 113 | 114 | dashboard, err := container.New(db.terminal, dashboardConfig...) 115 | if err != nil { 116 | return fmt.Errorf("failed to initialize dashboard: %w", err) 117 | } 118 | 119 | db.dashboard = dashboard 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/peer.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "github.com/jedib0t/go-pretty/v6/text" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | ) 8 | 9 | var ( 10 | ColumnsConfigPeer = ColumnsConfig{ 11 | enums.ColumnNameIndex: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 12 | enums.ColumnNameHealth: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 13 | enums.ColumnNameAddress: NewDefaultColumnConfig(text.AlignLeft, text.AlignCenter, false), 14 | enums.ColumnNamePortRPC: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 15 | enums.ColumnNameTotalTransactionBlocks: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 16 | enums.ColumnNameLatestCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 17 | enums.ColumnNameTotalTransactionCertificates: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 18 | enums.ColumnNameTotalTransactionEffects: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 19 | enums.ColumnNameHighestKnownCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 20 | enums.ColumnNameHighestSyncedCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 21 | enums.ColumnNameLastExecutedCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 22 | enums.ColumnNameCheckpointExecBacklog: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 23 | enums.ColumnNameCheckpointSyncBacklog: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 24 | enums.ColumnNameCurrentEpoch: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 25 | enums.ColumnNameTXSyncPercentage: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 26 | enums.ColumnNameCheckSyncPercentage: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 27 | enums.ColumnNameNetworkPeers: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 28 | enums.ColumnNameUptime: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 29 | enums.ColumnNameVersion: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 30 | enums.ColumnNameCommit: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 31 | enums.ColumnNameCountry: NewDefaultColumnConfig(text.AlignLeft, text.AlignCenter, false), 32 | } 33 | 34 | RowsConfigPeer = RowsConfig{ 35 | 0: { 36 | enums.ColumnNameIndex, 37 | enums.ColumnNameHealth, 38 | enums.ColumnNameAddress, 39 | enums.ColumnNamePortRPC, 40 | enums.ColumnNameTotalTransactionBlocks, 41 | enums.ColumnNameLatestCheckpoint, 42 | enums.ColumnNameTotalTransactionCertificates, 43 | enums.ColumnNameTotalTransactionEffects, 44 | enums.ColumnNameHighestKnownCheckpoint, 45 | enums.ColumnNameLastExecutedCheckpoint, 46 | enums.ColumnNameCheckpointExecBacklog, 47 | enums.ColumnNameHighestSyncedCheckpoint, 48 | enums.ColumnNameCheckpointSyncBacklog, 49 | }, 50 | 1: { 51 | enums.ColumnNameCurrentEpoch, 52 | enums.ColumnNameTXSyncPercentage, 53 | enums.ColumnNameCheckSyncPercentage, 54 | enums.ColumnNameNetworkPeers, 55 | enums.ColumnNameUptime, 56 | enums.ColumnNameVersion, 57 | enums.ColumnNameCommit, 58 | enums.ColumnNameCountry, 59 | }, 60 | } 61 | ) 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bartosian/suimon 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/AlecAivazis/survey/v2 v2.3.7 9 | github.com/dariubs/percent v1.0.0 10 | github.com/docker/docker v27.3.1+incompatible 11 | github.com/fatih/color v1.17.0 12 | github.com/glendc/go-external-ip v0.1.0 13 | github.com/hashicorp/go-multierror v1.1.1 14 | github.com/ipinfo/go/v2 v2.10.0 15 | github.com/jedib0t/go-pretty/v6 v6.6.0 16 | github.com/mum4k/termdash v0.20.0 17 | github.com/prometheus/client_golang v1.20.4 18 | github.com/prometheus/client_model v0.6.1 19 | github.com/prometheus/common v0.60.0 20 | github.com/schollz/progressbar/v3 v3.16.1 21 | github.com/shirou/gopsutil/v3 v3.24.5 22 | github.com/spf13/cobra v1.8.1 23 | github.com/ybbus/jsonrpc/v3 v3.1.5 24 | golang.org/x/sync v0.8.0 25 | gopkg.in/yaml.v3 v3.0.1 26 | ) 27 | 28 | require ( 29 | github.com/Microsoft/go-winio v0.6.2 // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/containerd/log v0.1.0 // indirect 33 | github.com/creack/pty v1.1.18 // indirect 34 | github.com/distribution/reference v0.6.0 // indirect 35 | github.com/docker/go-connections v0.5.0 // indirect 36 | github.com/docker/go-units v0.5.0 // indirect 37 | github.com/felixge/httpsnoop v1.0.4 // indirect 38 | github.com/go-logr/logr v1.4.2 // indirect 39 | github.com/go-logr/stdr v1.2.2 // indirect 40 | github.com/go-ole/go-ole v1.3.0 // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/hashicorp/errwrap v1.1.0 // indirect 43 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 44 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 45 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect 46 | github.com/mattn/go-colorable v0.1.13 // indirect 47 | github.com/mattn/go-isatty v0.0.20 // indirect 48 | github.com/mattn/go-runewidth v0.0.16 // indirect 49 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 50 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 51 | github.com/moby/docker-image-spec v1.3.1 // indirect 52 | github.com/moby/term v0.5.0 // indirect 53 | github.com/morikuni/aec v1.0.0 // indirect 54 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 55 | github.com/nsf/termbox-go v1.1.1 // indirect 56 | github.com/opencontainers/go-digest v1.0.0 // indirect 57 | github.com/opencontainers/image-spec v1.1.0 // indirect 58 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 59 | github.com/pkg/errors v0.9.1 // indirect 60 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 61 | github.com/prometheus/procfs v0.15.1 // indirect 62 | github.com/rivo/uniseg v0.4.7 // indirect 63 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 64 | github.com/spf13/pflag v1.0.5 // indirect 65 | github.com/tklauser/go-sysconf v0.3.14 // indirect 66 | github.com/tklauser/numcpus v0.9.0 // indirect 67 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 68 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect 69 | go.opentelemetry.io/otel v1.31.0 // indirect 70 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect 71 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 72 | go.opentelemetry.io/otel/sdk v1.31.0 // indirect 73 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 74 | golang.org/x/sys v0.26.0 // indirect 75 | golang.org/x/term v0.25.0 // indirect 76 | golang.org/x/text v0.19.0 // indirect 77 | golang.org/x/time v0.3.0 // indirect 78 | google.golang.org/protobuf v1.35.1 // indirect 79 | gotest.tools/v3 v3.4.0 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /internal/core/domain/host/host.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dariubs/percent" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | "github.com/bartosian/suimon/internal/core/domain/metrics" 10 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 11 | "github.com/bartosian/suimon/internal/core/ports" 12 | ) 13 | 14 | const percentage100 = 100 15 | 16 | type Gateways struct { 17 | rpc ports.RPCGateway 18 | geo ports.GeoGateway 19 | prometheus ports.PrometheusGateway 20 | cli *cligw.Gateway 21 | } 22 | 23 | type Host struct { 24 | AddressInfo 25 | 26 | gateways Gateways 27 | IPInfo *ports.IPResult 28 | 29 | TableType enums.TableType 30 | 31 | Status enums.Status 32 | Metrics metrics.Metrics 33 | } 34 | 35 | func NewHost( 36 | tableType enums.TableType, 37 | addressInfo AddressInfo, 38 | rpcGW ports.RPCGateway, 39 | geoGW ports.GeoGateway, 40 | prometheusGW ports.PrometheusGateway, 41 | cliGW *cligw.Gateway, 42 | ) *Host { 43 | host := &Host{ 44 | TableType: tableType, 45 | AddressInfo: addressInfo, 46 | gateways: Gateways{ 47 | rpc: rpcGW, 48 | geo: geoGW, 49 | prometheus: prometheusGW, 50 | cli: cliGW, 51 | }, 52 | } 53 | 54 | return host 55 | } 56 | 57 | // SetPctProgress updates the value of the specified metric type for the Host instance with a percentage that reflects the Host's progress relative to the progress of the RPC Host. 58 | // The function obtains the current metric value for the Host and RPC Host, calculates the percentage using the percent.PercentOf function, and sets the new percentage value for the Host's Metrics instance for the specified metric type. 59 | // The second argument is the RPC Host to compare the progress against. 60 | func (host *Host) SetPctProgress(metricType enums.MetricType, rpc *Host) error { 61 | hostMetric := host.Metrics.GetValue(metricType) 62 | rpcMetric := rpc.Metrics.GetValue(metricType) 63 | 64 | hostMetricInt, ok := hostMetric.(int) 65 | if !ok { 66 | return fmt.Errorf("failed to convert metric to INT") 67 | } 68 | 69 | rpcMetricInt, ok := rpcMetric.(int) 70 | if !ok { 71 | return fmt.Errorf("failed to convert metric to INT") 72 | } 73 | 74 | percentage := int(percent.PercentOf(hostMetricInt, rpcMetricInt)) 75 | if percentage > percentage100 { 76 | percentage = percentage100 77 | } 78 | 79 | return host.Metrics.SetValue(metricType, percentage) 80 | } 81 | 82 | // SetStatus updates the status of the Host based on the provided RPC Host. 83 | // It compares the metrics of the Host and RPC Host and sets the status to Red, Yellow, or Green based on specific conditions. 84 | func (host *Host) SetStatus(rpc *Host) { 85 | metricsHost := host.Metrics 86 | metricsRPC := rpc.Metrics 87 | 88 | if host.TableType == enums.TableTypeValidator { 89 | if !metricsHost.Updated || metricsHost.Uptime == "" { 90 | host.Status = enums.StatusRed 91 | return 92 | } 93 | } 94 | 95 | if host.TableType == enums.TableTypeNode || host.TableType == enums.TableTypeRPC { 96 | if !metricsHost.Updated { 97 | host.Status = enums.StatusRed 98 | return 99 | } 100 | 101 | if metricsHost.TotalTransactionsBlocks == 0 || 102 | metricsHost.LatestCheckpoint == 0 || 103 | (metricsHost.TransactionsPerSecond == 0 && len(metricsHost.TransactionsHistory) == metrics.TransactionsPerSecondWindow) || 104 | metricsHost.TxSyncPercentage == 0 || 105 | metricsHost.TxSyncPercentage > 110 || 106 | metricsHost.CheckSyncPercentage > 110 { 107 | host.Status = enums.StatusRed 108 | 109 | return 110 | } 111 | 112 | if metricsHost.IsUnhealthy(enums.MetricTypeTransactionsPerSecond, metricsRPC.TransactionsPerSecond) || 113 | metricsHost.IsUnhealthy(enums.MetricTypeTotalTransactionBlocks, metricsRPC.TotalTransactionsBlocks) || 114 | metricsHost.IsUnhealthy(enums.MetricTypeLatestCheckpoint, metricsRPC.LatestCheckpoint) { 115 | host.Status = enums.StatusYellow 116 | return 117 | } 118 | } 119 | 120 | host.Status = enums.StatusGreen 121 | } 122 | -------------------------------------------------------------------------------- /internal/core/domain/metrics/protocol.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | // Protocol represents the protocol information of the Sui blockchain network. 4 | // It includes the minimum and maximum supported protocol versions, the current protocol version, and the feature flags. 5 | type Protocol struct { 6 | MinSupportedProtocolVersion string `json:"minSupportedProtocolVersion"` 7 | MaxSupportedProtocolVersion string `json:"maxSupportedProtocolVersion"` 8 | ProtocolVersion string `json:"protocolVersion"` 9 | FeatureFlags FeatureFlags `json:"featureFlags"` 10 | } 11 | 12 | // FeatureFlags represents the various feature flags of the Sui blockchain network. 13 | // Each flag represents a specific feature of the network that can be toggled on or off. 14 | type FeatureFlags struct { 15 | AcceptZkloginInMultisig bool `json:"accept_zklogin_in_multisig"` 16 | AdvanceEpochStartTimeInSafeMode bool `json:"advance_epoch_start_time_in_safe_mode"` 17 | AdvanceToHighestSupportedProtocolVersion bool `json:"advance_to_highest_supported_protocol_version"` 18 | AllowReceivingObjectID bool `json:"allow_receiving_object_id"` 19 | BanEntryInit bool `json:"ban_entry_init"` 20 | CommitRootStateDigest bool `json:"commit_root_state_digest"` 21 | ConsensusOrderEndOfEpochLast bool `json:"consensus_order_end_of_epoch_last"` 22 | DisableInvariantViolationCheckInSwapLoc bool `json:"disable_invariant_violation_check_in_swap_loc"` 23 | DisallowAddingAbilitiesOnUpgrade bool `json:"disallow_adding_abilities_on_upgrade"` 24 | DisallowChangeStructTypeParamsOnUpgrade bool `json:"disallow_change_struct_type_params_on_upgrade"` 25 | EnableEffectsV2 bool `json:"enable_effects_v2"` 26 | EnableJwkConsensusUpdates bool `json:"enable_jwk_consensus_updates"` 27 | EndOfEpochTransactionSupported bool `json:"end_of_epoch_transaction_supported"` 28 | HardenedOtwCheck bool `json:"hardened_otw_check"` 29 | IncludeConsensusDigestInPrologue bool `json:"include_consensus_digest_in_prologue"` 30 | LoadedChildObjectFormat bool `json:"loaded_child_object_format"` 31 | LoadedChildObjectFormatType bool `json:"loaded_child_object_format_type"` 32 | LoadedChildObjectsFixed bool `json:"loaded_child_objects_fixed"` 33 | MissingTypeIsCompatibilityError bool `json:"missing_type_is_compatibility_error"` 34 | NarwhalCertificateV2 bool `json:"narwhal_certificate_v2"` 35 | NarwhalHeaderV2 bool `json:"narwhal_header_v2"` 36 | NarwhalNewLeaderElectionSchedule bool `json:"narwhal_new_leader_election_schedule"` 37 | NarwhalVersionedMetadata bool `json:"narwhal_versioned_metadata"` 38 | NoExtraneousModuleBytes bool `json:"no_extraneous_module_bytes"` 39 | PackageDigestHashModule bool `json:"package_digest_hash_module"` 40 | PackageUpgrades bool `json:"package_upgrades"` 41 | RandomBeacon bool `json:"random_beacon"` 42 | ReceiveObjects bool `json:"receive_objects"` 43 | RecomputeHasPublicTransferInExecution bool `json:"recompute_has_public_transfer_in_execution"` 44 | ScoringDecisionWithValidityCutoff bool `json:"scoring_decision_with_validity_cutoff"` 45 | SharedObjectDeletion bool `json:"shared_object_deletion"` 46 | SimpleConservationChecks bool `json:"simple_conservation_checks"` 47 | SimplifiedUnwrapThenDelete bool `json:"simplified_unwrap_then_delete"` 48 | ThroughputAwareConsensusSubmission bool `json:"throughput_aware_consensus_submission"` 49 | TxnBaseCostAsMultiplier bool `json:"txn_base_cost_as_multiplier"` 50 | UpgradedMultisigSupported bool `json:"upgraded_multisig_supported"` 51 | VerifyLegacyZkloginAddress bool `json:"verify_legacy_zklogin_address"` 52 | ZkloginAuth bool `json:"zklogin_auth"` 53 | } 54 | -------------------------------------------------------------------------------- /internal/core/domain/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | const ( 4 | TransactionsPerSecondWindow = 5 5 | CheckpointsPerSecondWindow = 5 6 | RoundsPerSecondWindow = 5 7 | CertificatesPerSecondWindow = 5 8 | TransactionsPerSecondLag = 5 9 | CheckpointsPerSecondLag = 10 10 | LatestCheckpointLag = 30 11 | HighestSyncedCheckpointLag = 30 12 | TotalTransactionsSyncPercentage = 99 13 | TotalCheckpointsSyncPercentage = 99 14 | ) 15 | 16 | type ( 17 | // Transactions represents information about transactions on the Sui blockchain network. 18 | Transactions struct { 19 | TransactionsHistory []int 20 | CertificatesHistory []int 21 | TotalTransactionsBlocks int 22 | TotalTransactionCertificates int 23 | TotalTransactionCertificatesCreated int 24 | CertificatesPerSecond int 25 | NonConsensusLatency int 26 | TotalTransactionEffects int 27 | TransactionsPerSecond int 28 | TxSyncPercentage int 29 | } 30 | 31 | // Checkpoints represents information about checkpoints on the Sui blockchain network. 32 | Checkpoints struct { 33 | CheckpointsHistory []int 34 | LatestCheckpoint int 35 | HighestKnownCheckpoint int 36 | HighestSyncedCheckpoint int 37 | LastExecutedCheckpoint int 38 | CheckpointsPerSecond int 39 | CheckpointExecBacklog int 40 | CheckpointSyncBacklog int 41 | CheckSyncPercentage int 42 | } 43 | 44 | // Rounds represents information about rounds on the Sui blockchain network. 45 | Rounds struct { 46 | RoundsHistory []int 47 | LastCommittedLeaderRound int 48 | HighestAcceptedRound int 49 | RoundsPerSecond int 50 | ConsensusRoundProberCurrentRoundGaps int 51 | } 52 | 53 | // Peers represents information about peers on the Sui blockchain network. 54 | Peers struct { 55 | NetworkPeers int 56 | } 57 | 58 | // Epoch represents information about the current epoch on the Sui blockchain network. 59 | Epoch struct { 60 | EpochStartTimeUTC string 61 | EpochDurationHHMM string 62 | DurationTillEpochEndHHMM string 63 | CurrentEpoch int 64 | EpochTotalDuration int 65 | EpochPercentage int 66 | TimeTillNextEpoch int64 67 | } 68 | 69 | // Errors represents information about errors on the Sui blockchain network. 70 | Errors struct { 71 | SkippedConsensusTransactions int 72 | TotalSignatureErrors int 73 | } 74 | 75 | // GasPrice represents the different reference gas prices used on the network. 76 | GasPrice struct { 77 | MinReferenceGasPrice int // The minimum gas price (in wei) that transactions should pay in order to be included in the next block. 78 | MaxReferenceGasPrice int // The maximum gas price (in wei) that transactions should pay in order to avoid overpaying and wasting funds. 79 | MeanReferenceGasPrice int // The average gas price (in wei) of transactions that were included in the last few blocks. 80 | StakeWeightedMeanReferenceGasPrice int // The average gas price (in wei) weighted by the amount of stake that each validator has on the network. 81 | MedianReferenceGasPrice int // The middle value of the sorted list of gas prices (in wei) that were included in the last few blocks. 82 | EstimatedNextReferenceGasPrice int // The gas price (in wei) that is estimated to be included in the next block based on recent network activity and congestion. 83 | } 84 | 85 | // Object represents information about objects on the Sui blockchain network. 86 | Objects struct { 87 | NumberSharedObjectTransactions int 88 | } 89 | 90 | // Metrics represents various metrics about the Sui blockchain network. 91 | Metrics struct { 92 | ValidatorsApyParsed ValidatorsApyParsed 93 | 94 | Uptime string 95 | Version string 96 | Commit string 97 | 98 | SystemState SuiSystemState 99 | 100 | CurrentVotingRight float64 101 | 102 | Epoch 103 | Protocol 104 | Rounds 105 | Objects 106 | Transactions 107 | Checkpoints 108 | GasPrice 109 | Peers 110 | Errors 111 | Updated bool 112 | } 113 | ) 114 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/render.go: -------------------------------------------------------------------------------- 1 | package dashboardbuilder 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/mum4k/termdash" 9 | "golang.org/x/sync/errgroup" 10 | 11 | "github.com/bartosian/suimon/internal/core/domain/service/dashboardbuilder/dashboards" 12 | ) 13 | 14 | const ( 15 | renderInterval = 200 * time.Millisecond 16 | queryInterval = 2500 * time.Millisecond 17 | ) 18 | 19 | // Render renders the dashboard by starting the query and rerender loops, 20 | // and waiting for them to complete. It returns an error if any of the loops 21 | // encounter an error. 22 | func (db *Builder) Render() (err error) { 23 | defer func() { 24 | if r := recover(); r != nil { 25 | db.cliGateway.Error(fmt.Sprintf("panic: %v", r)) 26 | db.tearDown() 27 | os.Exit(1) 28 | } else if err != nil { 29 | db.tearDown() 30 | } 31 | }() 32 | 33 | var errGroup errgroup.Group 34 | 35 | queryTicker, renderTicker := startTickers() 36 | defer stopTickers(queryTicker, renderTicker) 37 | 38 | errGroup.Go(queryMetricsLoop(db, queryTicker)) 39 | errGroup.Go(rerenderLoop(db, renderTicker)) 40 | errGroup.Go(runDashboard(db)) 41 | 42 | return errGroup.Wait() 43 | } 44 | 45 | func startTickers() (queryTicker, renderTicker *time.Ticker) { 46 | queryTicker = time.NewTicker(queryInterval) 47 | renderTicker = time.NewTicker(renderInterval) 48 | 49 | return 50 | } 51 | 52 | func stopTickers(tickers ...*time.Ticker) { 53 | for _, ticker := range tickers { 54 | ticker.Stop() 55 | } 56 | } 57 | 58 | // queryMetricsLoop fetches the metrics from the host at regular intervals. 59 | // It uses the provided ticker to trigger the fetch and returns an error if 60 | // the fetch encounters an error or if the context is done. 61 | // It returns a function that can be used to start the loop. 62 | // The loop can be stopped by canceling the context. 63 | // The function signature is compatible with the errgroup.Group.Go method. 64 | // The loop stops when the context is done. 65 | func queryMetricsLoop(db *Builder, ticker *time.Ticker) func() error { 66 | return func() error { 67 | for { 68 | select { 69 | case <-ticker.C: 70 | if err := db.host.GetMetrics(); err != nil { 71 | return err 72 | } 73 | case <-db.ctx.Done(): 74 | return nil 75 | } 76 | } 77 | } 78 | } 79 | 80 | // rerenderLoop continuously fetches the latest column values from the host at regular intervals. 81 | // It uses the provided ticker to trigger the fetch and updates the cells with the latest values. 82 | // The loop stops when the context is done. 83 | // The function signature is compatible with the errgroup.Group.Go method. 84 | // It returns a function that can be used to start the loop. 85 | func rerenderLoop(db *Builder, ticker *time.Ticker) func() error { 86 | return func() error { 87 | for { 88 | select { 89 | case <-ticker.C: 90 | columnValues, err := dashboards.GetColumnsValues(db.tableType, db.host) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | for columnName, cell := range db.cells { 96 | columnValue, ok := columnValues[columnName] 97 | if !ok { 98 | return fmt.Errorf("failed to get metric for column %s", columnName) 99 | } 100 | 101 | if writeErr := cell.Write(columnValue); writeErr != nil { 102 | return writeErr 103 | } 104 | } 105 | case <-db.ctx.Done(): 106 | return nil 107 | } 108 | } 109 | } 110 | } 111 | 112 | // runDashboard runs the dashboard using termdash.Run method. 113 | // It takes a Builder instance as input and returns a function that can be used to start the dashboard. 114 | // The returned function can be used to start the dashboard and handle keyboard events using the provided quitter function. 115 | // It returns an error if the dashboard fails to run. 116 | // The dashboard is run using the termdash.Run method with the provided context, terminal, dashboard, and keyboard subscriber. 117 | // The returned error indicates any failure during the dashboard run. 118 | // The function signature is compatible with the errgroup.Group.Go method. 119 | // It returns a function that can be used to start the dashboard. 120 | func runDashboard(db *Builder) func() error { 121 | return func() error { 122 | return termdash.Run(db.ctx, db.terminal, db.dashboard, termdash.KeyboardSubscriber(db.quitter)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/base.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/config" 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | "github.com/bartosian/suimon/internal/core/domain/host" 10 | "github.com/bartosian/suimon/internal/core/domain/metrics" 11 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 12 | "github.com/bartosian/suimon/internal/core/ports" 13 | ) 14 | 15 | type Gateways struct { 16 | cli *cligw.Gateway 17 | } 18 | 19 | type Hosts struct { 20 | rpc []host.Host 21 | node []host.Host 22 | validator []host.Host 23 | } 24 | 25 | type Releases []Releases 26 | 27 | type Builders struct { 28 | static map[enums.TableType]ports.Builder 29 | dynamic map[enums.TableType]ports.Builder 30 | } 31 | 32 | type Controller struct { 33 | builders Builders 34 | configs map[string]config.Config 35 | gateways Gateways 36 | selectedConfig config.Config 37 | network string 38 | selectedDashboard enums.TableType 39 | hosts Hosts 40 | releases []metrics.Release 41 | selectedTables []enums.TableType 42 | lock sync.RWMutex 43 | } 44 | 45 | // NewController creates a new instance of the Controller. 46 | // It takes a map of configuration and a CLI gateway as input and returns a pointer to the Controller. 47 | // The map of configuration is used to initialize the Controller's configs field. 48 | // The CLI gateway is used to initialize the Controller's gateways field. 49 | // The static and dynamic maps in the Builders field are initialized with empty maps. 50 | // The newly created Controller instance is returned. 51 | func NewController( 52 | configs map[string]config.Config, 53 | cliGW *cligw.Gateway, 54 | ) *Controller { 55 | return &Controller{ 56 | configs: configs, 57 | gateways: Gateways{ 58 | cli: cliGW, 59 | }, 60 | builders: Builders{ 61 | static: make(map[enums.TableType]ports.Builder), 62 | dynamic: make(map[enums.TableType]ports.Builder), 63 | }, 64 | } 65 | } 66 | 67 | // getHostsByTableType is a method of the Controller struct that returns a list of hosts for a given table type. 68 | // It acquires a read lock on the controller lock before accessing the hosts data. 69 | // The method uses a switch statement to determine which type of hosts to return based on the table type. 70 | // The method returns a list of hosts and an error. The error is returned if the table type is unknown or if there are no RPC hosts available for the specified table types. 71 | func (c *Controller) getHostsByTableType(table enums.TableType) (hosts []host.Host, err error) { 72 | c.lock.RLock() 73 | defer c.lock.RUnlock() 74 | 75 | switch table { 76 | case enums.TableTypeNode: 77 | return c.hosts.node, nil 78 | case enums.TableTypeValidator: 79 | return c.hosts.validator, nil 80 | case enums.TableTypeRPC: 81 | return c.hosts.rpc, nil 82 | case enums.TableTypeActiveValidators, 83 | enums.TableTypeGasPriceAndSubsidy, 84 | enums.TableTypeValidatorParams, 85 | enums.TableTypeValidatorsAtRisk, 86 | enums.TableTypeValidatorReports, 87 | enums.TableTypeProtocol: 88 | if len(c.hosts.rpc) > 0 { 89 | return c.hosts.rpc[:1], nil 90 | } 91 | 92 | return nil, fmt.Errorf("no rpc hosts available for table type: %v", table) 93 | case enums.TableTypeReleases: 94 | return nil, fmt.Errorf("no hosts available for table type: %v", table) 95 | default: 96 | return nil, fmt.Errorf("unknown table type: %v", table) 97 | } 98 | } 99 | 100 | // setHostsByTableType sets the list of hosts for a given table type. 101 | // It acquires a write lock on the controller lock before updating the hosts data. 102 | // If the table type is unknown, it returns an error. 103 | // The error is returned if the table type is unknown or if there is an error acquiring the lock. 104 | func (c *Controller) setHostsByTableType(table enums.TableType, hosts []host.Host) error { 105 | c.lock.Lock() 106 | defer c.lock.Unlock() 107 | 108 | switch table { 109 | case enums.TableTypeNode: 110 | c.hosts.node = hosts 111 | case enums.TableTypeValidator: 112 | c.hosts.validator = hosts 113 | case enums.TableTypeRPC: 114 | c.hosts.rpc = hosts 115 | case enums.TableTypeActiveValidators, 116 | enums.TableTypeGasPriceAndSubsidy, 117 | enums.TableTypeValidatorParams, 118 | enums.TableTypeValidatorsAtRisk, 119 | enums.TableTypeValidatorReports, 120 | enums.TableTypeProtocol, 121 | enums.TableTypeReleases: 122 | return nil 123 | default: 124 | return fmt.Errorf("unknown table type: %v", table) 125 | } 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/activevalidator.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "github.com/jedib0t/go-pretty/v6/text" 5 | 6 | "github.com/bartosian/suimon/internal/core/domain/enums" 7 | domainmetrics "github.com/bartosian/suimon/internal/core/domain/metrics" 8 | ) 9 | 10 | var ( 11 | ColumnsConfigActiveValidator = ColumnsConfig{ 12 | enums.ColumnNameIndex: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 13 | enums.ColumnNameValidatorName: NewDefaultColumnConfig(text.AlignLeft, text.AlignCenter, false), 14 | enums.ColumnNameValidatorNetAddress: NewDefaultColumnConfig(text.AlignLeft, text.AlignCenter, false), 15 | enums.ColumnNameValidatorVotingPower: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 16 | enums.ColumnNameValidatorGasPrice: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 17 | enums.ColumnNameValidatorCommissionRate: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 18 | enums.ColumnNameValidatorApy: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 19 | enums.ColumnNameValidatorNextEpochStake: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 20 | enums.ColumnNameValidatorNextEpochGasPrice: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 21 | enums.ColumnNameValidatorNextEpochCommissionRate: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 22 | enums.ColumnNameValidatorStakingPoolSuiBalance: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 23 | enums.ColumnNameValidatorRewardsPool: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 24 | enums.ColumnNameValidatorPoolTokenBalance: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 25 | enums.ColumnNameValidatorPendingStake: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 26 | enums.ColumnNameValidatorPendingTotalSuiWithdraw: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 27 | enums.ColumnNameValidatorPendingPoolTokenWithdraw: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 28 | } 29 | RowsActiveValidator = RowsConfig{ 30 | 0: { 31 | enums.ColumnNameIndex, 32 | enums.ColumnNameValidatorName, 33 | enums.ColumnNameValidatorVotingPower, 34 | enums.ColumnNameValidatorGasPrice, 35 | enums.ColumnNameValidatorCommissionRate, 36 | enums.ColumnNameValidatorApy, 37 | enums.ColumnNameValidatorNextEpochStake, 38 | enums.ColumnNameValidatorNextEpochGasPrice, 39 | enums.ColumnNameValidatorNextEpochCommissionRate, 40 | enums.ColumnNameValidatorStakingPoolSuiBalance, 41 | enums.ColumnNameValidatorRewardsPool, 42 | enums.ColumnNameValidatorPoolTokenBalance, 43 | enums.ColumnNameValidatorPendingStake, 44 | }, 45 | } 46 | ) 47 | 48 | // GetActiveValidatorColumnValues returns a map of ActiveValidatorColumnName values to corresponding values for the specified active validator. 49 | // The function retrieves information about the active validator from the provided metrics.Validator object and formats it into a map of ActiveValidatorColumnName keys and corresponding values. 50 | // Returns a map of ActiveValidatorColumnName keys to corresponding values. 51 | func GetActiveValidatorColumnValues(idx int, validator *domainmetrics.Validator) (ColumnValues, error) { 52 | result := ColumnValues{ 53 | enums.ColumnNameIndex: idx + 1, 54 | enums.ColumnNameValidatorName: validator.Name, 55 | enums.ColumnNameValidatorNetAddress: validator.NetAddress, 56 | enums.ColumnNameValidatorVotingPower: validator.VotingPower, 57 | enums.ColumnNameValidatorGasPrice: validator.GasPrice, 58 | enums.ColumnNameValidatorCommissionRate: validator.CommissionRate, 59 | enums.ColumnNameValidatorApy: validator.APY, 60 | enums.ColumnNameValidatorNextEpochGasPrice: validator.NextEpochGasPrice, 61 | enums.ColumnNameValidatorNextEpochCommissionRate: validator.NextEpochCommissionRate, 62 | enums.ColumnNameValidatorPendingTotalSuiWithdraw: validator.PendingTotalSuiWithdraw, 63 | enums.ColumnNameValidatorPendingPoolTokenWithdraw: validator.PendingPoolTokenWithdraw, 64 | } 65 | 66 | mistValues := map[enums.ColumnName]string{ 67 | enums.ColumnNameValidatorNextEpochStake: validator.NextEpochStake, 68 | enums.ColumnNameValidatorStakingPoolSuiBalance: validator.StakingPoolSuiBalance, 69 | enums.ColumnNameValidatorRewardsPool: validator.RewardsPool, 70 | enums.ColumnNameValidatorPoolTokenBalance: validator.PoolTokenBalance, 71 | enums.ColumnNameValidatorPendingStake: validator.PendingStake, 72 | } 73 | 74 | for columnName, mistValue := range mistValues { 75 | intValue, err := domainmetrics.MistToSui(mistValue) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | result[columnName] = intValue 81 | } 82 | 83 | return result, nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/core/domain/metrics/getvalue.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/dariubs/percent" 9 | 10 | "github.com/bartosian/suimon/internal/core/domain/enums" 11 | ) 12 | 13 | const secondsInHour = 60 14 | 15 | type MetricValue interface{} 16 | 17 | // GetValue returns the metric value for the given metric type. 18 | // 19 | //nolint:gocyclo // temporary disabled. 20 | func (metrics *Metrics) GetValue(metric enums.MetricType) MetricValue { 21 | switch metric { 22 | case enums.MetricTypeSuiSystemState: 23 | return metrics.SystemState 24 | case enums.MetricTypeTotalTransactionBlocks: 25 | return metrics.TotalTransactionsBlocks 26 | case enums.MetricTypeTotalTransactionCertificates: 27 | return metrics.TotalTransactionCertificatesCreated 28 | case enums.MetricTypeTotalTransactionEffects: 29 | return metrics.TotalTransactionEffects 30 | case enums.MetricTypeTransactionsPerSecond: 31 | return metrics.TransactionsPerSecond 32 | case enums.MetricTypeLatestCheckpoint: 33 | return metrics.LatestCheckpoint 34 | case enums.MetricTypeHighestKnownCheckpoint: 35 | return metrics.HighestKnownCheckpoint 36 | case enums.MetricTypeHighestSyncedCheckpoint: 37 | return metrics.HighestSyncedCheckpoint 38 | case enums.MetricTypeLastExecutedCheckpoint: 39 | return metrics.LastExecutedCheckpoint 40 | case enums.MetricTypeCheckpointExecBacklog: 41 | return metrics.CheckpointExecBacklog 42 | case enums.MetricTypeCheckpointSyncBacklog: 43 | return metrics.CheckpointSyncBacklog 44 | case enums.MetricTypeCheckpointsPerSecond: 45 | return metrics.CheckpointsPerSecond 46 | case enums.MetricTypeCurrentEpoch: 47 | return metrics.CurrentEpoch 48 | case enums.MetricTypeEpochTotalDuration: 49 | return metrics.EpochTotalDuration 50 | case enums.MetricTypeTimeTillNextEpoch: 51 | return metrics.TimeTillNextEpoch 52 | case enums.MetricTypeTxSyncPercentage: 53 | return metrics.TotalTransactionsBlocks 54 | case enums.MetricTypeCheckSyncPercentage: 55 | return metrics.HighestSyncedCheckpoint 56 | case enums.MetricTypeSuiNetworkPeers: 57 | return metrics.NetworkPeers 58 | case enums.MetricTypeUptime: 59 | return metrics.Uptime 60 | case enums.MetricTypeVersion: 61 | return metrics.Version 62 | case enums.MetricTypeCommit: 63 | return metrics.Commit 64 | case enums.MetricTypeConsensusRoundProberCurrentRoundGaps: 65 | return metrics.ConsensusRoundProberCurrentRoundGaps 66 | case enums.MetricTypeTotalTransactionCertificatesCreated: 67 | return metrics.TotalTransactionCertificatesCreated 68 | case enums.MetricTypeSkippedConsensusTransactions: 69 | return metrics.SkippedConsensusTransactions 70 | case enums.MetricTypeTotalSignatureErrors: 71 | return metrics.TotalSignatureErrors 72 | case enums.MetricTypeNonConsensusLatencySum: 73 | return metrics.NonConsensusLatency 74 | case enums.MetricTypeValidatorsApy: 75 | return nil 76 | case enums.MetricTypeProtocol: 77 | return nil 78 | case enums.MetricTypeCurrentVotingRight: 79 | return metrics.CurrentVotingRight 80 | case enums.MetricTypeConsensusLastCommittedLeaderRound: 81 | return metrics.LastCommittedLeaderRound 82 | case enums.MetricTypeConsensusHighestAcceptedRound: 83 | return metrics.HighestAcceptedRound 84 | case enums.MetricTypeNumberSharedObjectTransactions: 85 | return metrics.NumberSharedObjectTransactions 86 | default: 87 | return nil 88 | } 89 | } 90 | 91 | // GetMillisecondsTillNextEpoch returns the milliseconds till the next epoch. 92 | func (metrics *Metrics) GetMillisecondsTillNextEpoch() (int64, error) { 93 | epochStartMs, err := strconv.ParseInt(metrics.SystemState.EpochStartTimestampMs, 10, 64) 94 | if err != nil { 95 | return 0, err 96 | } 97 | 98 | epochDurationMs, err := strconv.ParseInt(metrics.SystemState.EpochDurationMs, 10, 64) 99 | if err != nil { 100 | return 0, err 101 | } 102 | 103 | nextEpochStartMs := epochStartMs + epochDurationMs 104 | currentTimeMs := time.Now().UnixNano() / int64(time.Millisecond) 105 | 106 | return nextEpochStartMs - currentTimeMs, nil 107 | } 108 | 109 | // GetTimeUntilNextEpochDisplay returns the remaining time till the next epoch in human-readable format. 110 | func (metrics *Metrics) GetTimeUntilNextEpochDisplay() []string { 111 | duration := time.Duration(metrics.TimeTillNextEpoch) * time.Millisecond 112 | hours := int(duration.Hours()) 113 | minutes := int(duration.Minutes()) - (hours * secondsInHour) 114 | second := time.Now().Second() 115 | 116 | if hours < 0 { 117 | return nil 118 | } 119 | 120 | spacer := " " 121 | if second%2 == 0 { 122 | spacer = ":" 123 | } 124 | 125 | return []string{fmt.Sprintf("%02d%s%02d", hours, spacer, minutes), "H"} 126 | } 127 | 128 | // GetEpochLabel returns a string representing the current epoch number. 129 | func (metrics *Metrics) GetEpochLabel() string { 130 | return fmt.Sprintf("EPOCH %s", metrics.SystemState.Epoch) 131 | } 132 | 133 | // GetEpochProgress calculates and returns the percentage of current epoch progress. 134 | func (metrics *Metrics) GetEpochProgress() (int, error) { 135 | epochDurationMs, err := strconv.ParseInt(metrics.SystemState.EpochDurationMs, 10, 64) 136 | if err != nil { 137 | return 0, err 138 | } 139 | 140 | epochCurrentLength := epochDurationMs - metrics.TimeTillNextEpoch 141 | progressPercent := percent.PercentOf(int(epochCurrentLength), int(epochDurationMs)) 142 | 143 | return int(progressPercent), nil 144 | } 145 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | official@bartestnet.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /internal/core/gateways/prometheusgw/callfor.go: -------------------------------------------------------------------------------- 1 | package prometheusgw 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | ioPrometheusClient "github.com/prometheus/client_model/go" 10 | "github.com/prometheus/common/expfmt" 11 | 12 | "github.com/bartosian/suimon/internal/core/domain/enums" 13 | "github.com/bartosian/suimon/internal/core/ports" 14 | ) 15 | 16 | type MetricsData map[string]*ioPrometheusClient.MetricFamily 17 | 18 | type responseWithError struct { 19 | response *http.Response 20 | err error 21 | } 22 | 23 | var extractMetricValue = map[enums.PrometheusMetricType]func(metric *ioPrometheusClient.Metric) float64{ 24 | enums.PrometheusMetricTypeGauge: func(metric *ioPrometheusClient.Metric) float64 { return metric.GetGauge().GetValue() }, 25 | enums.PrometheusMetricTypeCounter: func(metric *ioPrometheusClient.Metric) float64 { return metric.GetCounter().GetValue() }, 26 | enums.PrometheusMetricTypeSummary: func(metric *ioPrometheusClient.Metric) float64 { return metric.GetSummary().GetSampleSum() }, 27 | enums.PrometheusMetricTypeHistogram: func(metric *ioPrometheusClient.Metric) float64 { return metric.GetHistogram().GetSampleSum() }, 28 | enums.PrometheusMetricTypeUntyped: func(metric *ioPrometheusClient.Metric) float64 { return metric.GetUntyped().GetValue() }, 29 | } 30 | 31 | // CallFor makes an HTTP request to the specified gateway URL to fetch metrics. 32 | // It returns the metrics result or an error if something goes wrong. 33 | func (gateway *Gateway) CallFor(metrics ports.Metrics) (result ports.MetricsResult, err error) { 34 | if len(metrics) == 0 { 35 | return nil, fmt.Errorf("no metrics provided") 36 | } 37 | 38 | req, err := http.NewRequest("GET", gateway.url, http.NoBody) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | ctx, cancel := context.WithTimeout(gateway.ctx, httpClientTimeout) 44 | defer cancel() 45 | 46 | req = req.WithContext(ctx) 47 | 48 | respChan := make(chan responseWithError, 1) 49 | 50 | go func() { 51 | //nolint:bodyclose // The response body is closed below to handle the response properly. 52 | resp, reqErr := gateway.client.Do(req) 53 | 54 | respChan <- responseWithError{response: resp, err: reqErr} 55 | }() 56 | 57 | select { 58 | case <-ctx.Done(): 59 | return nil, fmt.Errorf("http call timed out: %w", ctx.Err()) 60 | case result := <-respChan: 61 | if result.err != nil { 62 | return nil, fmt.Errorf("failed to get response from http client: %w", result.err) 63 | } 64 | 65 | response := result.response 66 | if response != nil { 67 | defer func() { 68 | if closeErr := response.Body.Close(); closeErr != nil { 69 | err = fmt.Errorf("failed to close response body: %w", closeErr) 70 | } 71 | }() 72 | } 73 | 74 | if response.StatusCode != http.StatusOK { 75 | return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) 76 | } 77 | 78 | parser := expfmt.TextParser{} 79 | 80 | data, parseErr := parser.TextToMetricFamilies(response.Body) 81 | if parseErr != nil { 82 | return nil, parseErr 83 | } 84 | 85 | metricsResult := make(ports.MetricsResult) 86 | 87 | for metricName, metricConfig := range metrics { 88 | result, getMetricValueErr := getMetricValueWithLabelFiltering(data, metricName.ToString(), metricConfig) 89 | if getMetricValueErr != nil { 90 | return nil, getMetricValueErr 91 | } 92 | 93 | metricsResult[metricName] = result 94 | } 95 | 96 | return metricsResult, nil 97 | } 98 | } 99 | 100 | // getMetricValueWithLabelFiltering searches for a specific metric in the provided MetricsData 101 | // by the given metricName and metricConfig. If a matching metric is found, it returns 102 | // the metric result containing its value and labels. If no matching metric is found, it returns an error. 103 | func getMetricValueWithLabelFiltering(metrics MetricsData, metricName string, metricConfig ports.MetricConfig) (result ports.MetricResult, err error) { 104 | if metrics == nil { 105 | return result, fmt.Errorf("no metrics provided") 106 | } 107 | 108 | metricType, labels := metricConfig.MetricType, metricConfig.Labels 109 | 110 | if _, validType := extractMetricValue[metricType]; !validType { 111 | return result, fmt.Errorf("invalid metric type: %v", metricType) 112 | } 113 | 114 | metricFamily, found := metrics[metricName] 115 | if !found { 116 | return result, fmt.Errorf("no metric family found for metric: %s with type: %v", metricName, metricType) 117 | } 118 | 119 | for _, metric := range metricFamily.Metric { 120 | if matchLabels(labels, metric.Label) { 121 | result.Labels = extractLabels(metric.Label) 122 | result.Value = extractMetricValue[metricType](metric) 123 | 124 | return result, nil 125 | } 126 | } 127 | 128 | return result, fmt.Errorf("no metric found matching labels: %v", labels) 129 | } 130 | 131 | // matchLabels checks if all labels match. 132 | func matchLabels(expected prometheus.Labels, actual []*ioPrometheusClient.LabelPair) bool { 133 | for key, value := range expected { 134 | if !labelMatches(key, value, actual) { 135 | return false 136 | } 137 | } 138 | 139 | return true 140 | } 141 | 142 | // extractLabels extracts labels from the metric. 143 | func extractLabels(labels []*ioPrometheusClient.LabelPair) prometheus.Labels { 144 | labelsResult := make(prometheus.Labels, len(labels)) 145 | for _, label := range labels { 146 | labelsResult[label.GetName()] = label.GetValue() 147 | } 148 | 149 | return labelsResult 150 | } 151 | 152 | // labelMatches checks if the key-value pair exists in the given labels. 153 | func labelMatches(key, value string, labels []*ioPrometheusClient.LabelPair) bool { 154 | for _, label := range labels { 155 | if label.GetName() == key && label.GetValue() == value { 156 | return true 157 | } 158 | } 159 | 160 | return false 161 | } 162 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/widget.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mum4k/termdash/align" 7 | "github.com/mum4k/termdash/cell" 8 | "github.com/mum4k/termdash/linestyle" 9 | "github.com/mum4k/termdash/widgetapi" 10 | "github.com/mum4k/termdash/widgets/gauge" 11 | "github.com/mum4k/termdash/widgets/segmentdisplay" 12 | "github.com/mum4k/termdash/widgets/sparkline" 13 | "github.com/mum4k/termdash/widgets/text" 14 | 15 | "github.com/bartosian/suimon/internal/core/domain/enums" 16 | ) 17 | 18 | const gaugeHeight4 = 4 19 | const gaugeTreshold99 = 99 20 | const maxLenBlinkValue = 50 21 | 22 | // newWidgetOfType initializes a new widget of the given type. 23 | // It returns the new widget and an error, if any. 24 | func newWidgetOfType(widgetType enums.WidgetType, color cell.Color) (widgetapi.Widget, error) { 25 | switch widgetType { 26 | case enums.WidgetTypeProgress: 27 | return newProgressWidget(color) 28 | case enums.WidgetTypeTextNoScroll: 29 | return newTextNoScrollWidget() 30 | case enums.WidgetTypeDisplay: 31 | return newDisplayWidget() 32 | case enums.WidgetTypeSparkLine: 33 | return newSparklineWidget(color) 34 | default: 35 | return nil, fmt.Errorf("invalid widget type: %d", widgetType) 36 | } 37 | } 38 | 39 | // newWidgetByColumnName initializes a new widget based on the given column name. 40 | // It returns the new widget and an error, if any. 41 | func newWidgetByColumnName(columnName enums.ColumnName, color cell.Color) (widgetapi.Widget, error) { 42 | //nolint: exhaustive,gocritic // no need to cover all cases here. 43 | switch columnName { 44 | case enums.ColumnNameTXSyncPercentage, enums.ColumnNameCheckSyncPercentage: 45 | widget, err := newWidgetOfType(enums.WidgetTypeProgress, color) 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to initialize gauge widget for %s: %w", columnName, err) 48 | } 49 | 50 | progressWidget, ok := widget.(*gauge.Gauge) 51 | if !ok { 52 | return nil, fmt.Errorf("failed to cast widget type") 53 | } 54 | 55 | if err = progressWidget.Percent(0); err != nil { 56 | return nil, fmt.Errorf("failed to set initial value for %s: %w", columnName, err) 57 | } 58 | 59 | return widget, nil 60 | case enums.ColumnNameHealth: 61 | widget, err := newWidgetOfType(enums.WidgetTypeTextNoScroll, color) 62 | if err != nil { 63 | return nil, fmt.Errorf("failed to initialize text widget for %s: %w", columnName, err) 64 | } 65 | 66 | textWidget, ok := widget.(*text.Text) 67 | if !ok { 68 | return nil, fmt.Errorf("failed to cast widget type") 69 | } 70 | 71 | if err = textWidget.Write(enums.StatusGrey.DashboardStatus(), text.WriteCellOpts(cell.FgColor(cell.ColorGray), cell.BgColor(cell.ColorGray))); err != nil { 72 | return nil, fmt.Errorf("failed to set initial value for %s: %w", columnName, err) 73 | } 74 | 75 | return widget, nil 76 | case enums.ColumnNameCheckpointsPerSecond, enums.ColumnNameTransactionsPerSecond, enums.ColumnNameRoundsPerSecond, enums.ColumnNameCertificatesPerSecond: 77 | widget, err := newWidgetOfType(enums.WidgetTypeSparkLine, color) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to initialize text widget for %s: %w", columnName, err) 80 | } 81 | 82 | sparkLineWidget, ok := widget.(*sparkline.SparkLine) 83 | if !ok { 84 | return nil, fmt.Errorf("failed to cast widget type") 85 | } 86 | 87 | if err = sparkLineWidget.Add([]int{0}); err != nil { 88 | return nil, fmt.Errorf("failed to set initial value for %s: %w", columnName, err) 89 | } 90 | 91 | return widget, nil 92 | default: 93 | widget, err := newWidgetOfType(enums.WidgetTypeDisplay, color) 94 | if err != nil { 95 | return nil, fmt.Errorf("failed to initialize segment display widget for %s: %w", columnName, err) 96 | } 97 | 98 | displayWidget, ok := widget.(*segmentdisplay.SegmentDisplay) 99 | if !ok { 100 | return nil, fmt.Errorf("failed to cast widget type") 101 | } 102 | 103 | err = displayWidget.Write([]*segmentdisplay.TextChunk{ 104 | segmentdisplay.NewChunk(dashboardLoadingBlinkValue(maxLenBlinkValue), segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorWhite))), 105 | }) 106 | if err != nil { 107 | return nil, fmt.Errorf("failed to set initial value for %s: %w", columnName, err) 108 | } 109 | 110 | return widget, nil 111 | } 112 | } 113 | 114 | // newProgressWidget initializes a new progress widget with the given options. 115 | // It returns the new widget and an error, if any. 116 | func newProgressWidget(color cell.Color) (*gauge.Gauge, error) { 117 | return gauge.New( 118 | gauge.Height(gaugeHeight4), 119 | gauge.Border(linestyle.Light, cell.FgColor(color)), 120 | gauge.Color(color), 121 | gauge.FilledTextColor(cell.ColorBlack), 122 | gauge.EmptyTextColor(cell.ColorWhite), 123 | gauge.HorizontalTextAlign(align.HorizontalCenter), 124 | gauge.VerticalTextAlign(align.VerticalMiddle), 125 | gauge.Threshold(gaugeTreshold99, linestyle.Double, cell.FgColor(cell.ColorRed), cell.Bold()), 126 | ) 127 | } 128 | 129 | // newTextNoScrollWidget initializes a new text widget that disables scrolling and wraps at rune boundaries. 130 | // It returns the new widget and an error, if any. 131 | func newTextNoScrollWidget() (*text.Text, error) { 132 | return text.New(text.DisableScrolling(), text.WrapAtRunes()) 133 | } 134 | 135 | // newSparklineWidget initializes a new sparkline widget with the given label and color. 136 | // It returns the new widget and an error, if any. 137 | func newSparklineWidget(color cell.Color) (*sparkline.SparkLine, error) { 138 | return sparkline.New(sparkline.Color(color)) 139 | } 140 | 141 | // newDisplayWidget initializes a new segment display widget with default options. 142 | // It returns the new widget and an error, if any. 143 | func newDisplayWidget() (*segmentdisplay.SegmentDisplay, error) { 144 | return segmentdisplay.New( 145 | segmentdisplay.AlignHorizontal(align.HorizontalCenter), 146 | segmentdisplay.AlignVertical(align.VerticalMiddle), 147 | segmentdisplay.MaximizeDisplayedText(), 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/node.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jedib0t/go-pretty/v6/text" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 10 | ) 11 | 12 | var ColumnsConfigNode = ColumnsConfig{ 13 | enums.ColumnNameIndex: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 14 | enums.ColumnNameHealth: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 15 | enums.ColumnNameAddress: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 16 | enums.ColumnNamePortRPC: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 17 | enums.ColumnNameTotalTransactionBlocks: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 18 | enums.ColumnNameLatestCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 19 | enums.ColumnNameTotalTransactionCertificates: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 20 | enums.ColumnNameTotalTransactionEffects: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 21 | enums.ColumnNameHighestKnownCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 22 | enums.ColumnNameHighestSyncedCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 23 | enums.ColumnNameLastExecutedCheckpoint: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 24 | enums.ColumnNameCheckpointExecBacklog: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 25 | enums.ColumnNameCheckpointSyncBacklog: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 26 | enums.ColumnNameCurrentEpoch: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 27 | enums.ColumnNameTXSyncPercentage: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 28 | enums.ColumnNameCheckSyncPercentage: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 29 | enums.ColumnNameNetworkPeers: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 30 | enums.ColumnNameUptime: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 31 | enums.ColumnNameVersion: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 32 | enums.ColumnNameCommit: NewDefaultColumnConfig(text.AlignCenter, text.AlignCenter, false), 33 | enums.ColumnNameCountry: NewDefaultColumnConfig(text.AlignCenter, text.AlignLeft, false), 34 | } 35 | 36 | var RowsConfigNode = RowsConfig{ 37 | 0: { 38 | enums.ColumnNameIndex, 39 | enums.ColumnNameHealth, 40 | enums.ColumnNameAddress, 41 | enums.ColumnNamePortRPC, 42 | enums.ColumnNameTotalTransactionBlocks, 43 | enums.ColumnNameLatestCheckpoint, 44 | enums.ColumnNameTotalTransactionCertificates, 45 | enums.ColumnNameTotalTransactionEffects, 46 | enums.ColumnNameHighestKnownCheckpoint, 47 | enums.ColumnNameLastExecutedCheckpoint, 48 | enums.ColumnNameCheckpointExecBacklog, 49 | enums.ColumnNameHighestSyncedCheckpoint, 50 | enums.ColumnNameCheckpointSyncBacklog, 51 | }, 52 | 1: { 53 | enums.ColumnNameCurrentEpoch, 54 | enums.ColumnNameTXSyncPercentage, 55 | enums.ColumnNameCheckSyncPercentage, 56 | enums.ColumnNameNetworkPeers, 57 | enums.ColumnNameUptime, 58 | enums.ColumnNameVersion, 59 | enums.ColumnNameCommit, 60 | enums.ColumnNameCountry, 61 | }, 62 | } 63 | 64 | // GetNodeColumnValues returns a map of NodeColumnName values to corresponding values for a node at the specified index on the specified host. 65 | // The function retrieves information about the node from the host's internal state and formats it into a map of NodeColumnName keys and corresponding values. 66 | // The function also includes emoji values in the map if the specified flag is true. 67 | // Returns a map of NodeColumnName keys to corresponding values. 68 | func GetNodeColumnValues(idx int, host *domainhost.Host) ColumnValues { 69 | status := host.Status.StatusToPlaceholder() 70 | 71 | var country string 72 | if host.IPInfo != nil { 73 | country = host.IPInfo.CountryName 74 | } 75 | 76 | port := host.Ports[enums.PortTypeRPC] 77 | if port == "" { 78 | port = RPCPortDefault 79 | } 80 | 81 | address := host.Endpoint.Address 82 | 83 | columnValues := ColumnValues{ 84 | enums.ColumnNameIndex: idx + 1, 85 | enums.ColumnNameHealth: status, 86 | enums.ColumnNameAddress: address, 87 | enums.ColumnNamePortRPC: port, 88 | enums.ColumnNameTotalTransactionBlocks: host.Metrics.TotalTransactionsBlocks, 89 | enums.ColumnNameTotalTransactionCertificates: host.Metrics.TotalTransactionCertificates, 90 | enums.ColumnNameTotalTransactionEffects: host.Metrics.TotalTransactionEffects, 91 | enums.ColumnNameLatestCheckpoint: host.Metrics.LatestCheckpoint, 92 | enums.ColumnNameHighestKnownCheckpoint: host.Metrics.HighestKnownCheckpoint, 93 | enums.ColumnNameHighestSyncedCheckpoint: host.Metrics.HighestSyncedCheckpoint, 94 | enums.ColumnNameLastExecutedCheckpoint: host.Metrics.LastExecutedCheckpoint, 95 | enums.ColumnNameCheckpointExecBacklog: host.Metrics.CheckpointExecBacklog, 96 | enums.ColumnNameCheckpointSyncBacklog: host.Metrics.CheckpointSyncBacklog, 97 | enums.ColumnNameCurrentEpoch: host.Metrics.CurrentEpoch, 98 | enums.ColumnNameTXSyncPercentage: fmt.Sprintf("%v%%", host.Metrics.TxSyncPercentage), 99 | enums.ColumnNameCheckSyncPercentage: fmt.Sprintf("%v%%", host.Metrics.CheckSyncPercentage), 100 | enums.ColumnNameNetworkPeers: host.Metrics.NetworkPeers, 101 | enums.ColumnNameUptime: host.Metrics.Uptime, 102 | enums.ColumnNameVersion: host.Metrics.Version, 103 | enums.ColumnNameCommit: host.Metrics.Commit, 104 | enums.ColumnNameCountry: country, 105 | } 106 | 107 | return columnValues 108 | } 109 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/base.go: -------------------------------------------------------------------------------- 1 | package tablebuilder 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/jedib0t/go-pretty/v6/table" 9 | "github.com/jedib0t/go-pretty/v6/text" 10 | 11 | "github.com/bartosian/suimon/internal/core/domain/enums" 12 | "github.com/bartosian/suimon/internal/core/domain/host" 13 | "github.com/bartosian/suimon/internal/core/domain/metrics" 14 | "github.com/bartosian/suimon/internal/core/domain/service/tablebuilder/tables" 15 | "github.com/bartosian/suimon/internal/core/gateways/cligw" 16 | ) 17 | 18 | const slashingPct50 = 50 19 | const slashingPct100 = 50 20 | 21 | type Builder struct { 22 | writer table.Writer 23 | cliGateway *cligw.Gateway 24 | config *tables.TableConfig 25 | tableType enums.TableType 26 | hosts []host.Host 27 | Releases []metrics.Release 28 | } 29 | 30 | // NewBuilder creates a new instance of the table builder, using the CLI gateway. 31 | func NewBuilder(tableType enums.TableType, hosts []host.Host, releases []metrics.Release, cliGateway *cligw.Gateway) *Builder { 32 | tableWR := table.NewWriter() 33 | tableWR.SetOutputMirror(os.Stdout) 34 | 35 | return &Builder{ 36 | tableType: tableType, 37 | Releases: releases, 38 | hosts: hosts, 39 | cliGateway: cliGateway, 40 | writer: tableWR, 41 | } 42 | } 43 | 44 | // setColumns sets the column configurations for the table builder based on the configuration in the builder's table config. 45 | func (tb *Builder) setColumns() { 46 | columnsConfig := make([]table.ColumnConfig, len(tb.config.Columns)) 47 | 48 | for _, column := range tb.config.Columns { 49 | columnsConfig = append(columnsConfig, *column.Config) 50 | } 51 | 52 | tb.writer.SetColumnConfigs(columnsConfig) 53 | } 54 | 55 | // setRows sets the rows of the table builder based on the configuration in the builder's table config. 56 | func (tb *Builder) setRows() error { 57 | rowsConfig := tb.config.Rows 58 | columnsConfig := tb.config.Columns 59 | itemsCount := tb.config.RowsCount 60 | columnsPerRow := len(rowsConfig[0]) 61 | 62 | // Prepare footer with empty values 63 | footer := tables.NewRow(tables.NewRowConfig{ 64 | IsHeader: false, 65 | IsFooter: true, 66 | Length: columnsPerRow, 67 | AutoMerge: true, 68 | AutoMergeAlign: text.AlignCenter, 69 | }) 70 | 71 | for itemIndex := 0; itemIndex < itemsCount; itemIndex++ { 72 | for rowIndex, columns := range rowsConfig { 73 | isFirstRow := itemIndex == 0 && rowIndex == 0 74 | isEvenRow := rowIndex%2 == 0 75 | multipleRows := len(rowsConfig) > 1 76 | 77 | header := tables.NewRow(tables.NewRowConfig{ 78 | IsHeader: true, 79 | IsFooter: false, 80 | Length: columnsPerRow, 81 | AutoMerge: true, 82 | AutoMergeAlign: text.AlignCenter, 83 | }) 84 | row := tables.NewRow(tables.NewRowConfig{ 85 | IsHeader: false, 86 | IsFooter: true, 87 | Length: columnsPerRow, 88 | AutoMerge: true, 89 | AutoMergeAlign: text.AlignCenter, 90 | }) 91 | 92 | // Build the row and header 93 | for _, columnName := range columns { 94 | columnConfig, ok := columnsConfig[columnName] 95 | if !ok { 96 | tb.cliGateway.Errorf("column %s not found", columnName) 97 | return fmt.Errorf("column %s not found", columnName) 98 | } 99 | 100 | columnValue := columnConfig.Values[itemIndex] 101 | 102 | if isFirstRow { 103 | header.AppendValue(columnName.ToString()) 104 | footer.PrependValue(tables.EmptyValue) 105 | } else if multipleRows && (rowIndex != 0 || itemIndex > 0 && isEvenRow) { 106 | header.AppendValue(columnName.ToString()) 107 | } 108 | 109 | row.AppendValue(columnValue) 110 | } 111 | 112 | // Handle empty spaces in rows 113 | for i := len(columns); i < columnsPerRow; i++ { 114 | emptyValue := tables.EmptyValue 115 | if isFirstRow { 116 | header.PrependValue(emptyValue) 117 | footer.PrependValue(emptyValue) 118 | } else if multipleRows && (rowIndex != 0 || itemIndex > 0 && isEvenRow) { 119 | header.PrependValue(emptyValue) 120 | } 121 | 122 | row.PrependValue(emptyValue) 123 | } 124 | 125 | // Append header and footer for the first row 126 | if isFirstRow { 127 | tb.writer.AppendHeader(header.Values, header.Config) 128 | tb.writer.AppendFooter(footer.Values, footer.Config) 129 | } else if multipleRows && (rowIndex != 0 || itemIndex > 0 && isEvenRow) { 130 | tb.writer.AppendRow(header.Values, header.Config) 131 | } 132 | 133 | // Append the row and separator 134 | tb.writer.AppendRow(row.Values, row.Config) 135 | tb.writer.AppendSeparator() 136 | } 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // setStyle sets the style for the table builder based on the configuration in the builder's table config. 143 | func (tb *Builder) setStyle() { 144 | tb.writer.SetTitle(tb.config.Name) 145 | tb.writer.SetStyle(tb.config.Style) 146 | 147 | tb.setColors() 148 | } 149 | 150 | // setColors sets the row colors for the table builder based on the current state of the table. 151 | func (tb *Builder) setColors() { 152 | var ( 153 | fgWhite = text.FgWhite 154 | bgWhite = text.BgWhite 155 | fgBlack = text.FgBlack 156 | bgRed = text.BgRed 157 | bgYellow = text.BgYellow 158 | ) 159 | 160 | var painter = func() func(row table.Row) text.Colors { 161 | valuesRowFgColor := text.Colors{fgWhite} 162 | 163 | var handler = func(row table.Row) text.Colors { 164 | if tb.tableType == enums.TableTypeValidatorReports { 165 | valueString, ok := row[1].(string) 166 | if !ok { 167 | return valuesRowFgColor 168 | } 169 | 170 | slashingPct, err := strconv.ParseFloat(valueString, 64) 171 | if err != nil { 172 | return valuesRowFgColor 173 | } 174 | 175 | if slashingPct > slashingPct100 { 176 | return text.Colors{bgRed, fgWhite} 177 | } 178 | 179 | if slashingPct > slashingPct50 { 180 | return text.Colors{bgYellow, fgBlack} 181 | } 182 | 183 | return valuesRowFgColor 184 | } 185 | 186 | for _, column := range row { 187 | switch value := column.(type) { 188 | case int, int16, int32, int64: 189 | return valuesRowFgColor 190 | case bool: 191 | return valuesRowFgColor 192 | case string: 193 | if _, err := strconv.Atoi(value); err == nil { 194 | return valuesRowFgColor 195 | } 196 | } 197 | } 198 | 199 | return text.Colors{fgBlack, bgWhite} 200 | } 201 | 202 | return handler 203 | }() 204 | 205 | tb.writer.SetRowPainter(painter) 206 | } 207 | -------------------------------------------------------------------------------- /internal/core/domain/service/tablebuilder/tables/base.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jedib0t/go-pretty/v6/table" 7 | "github.com/jedib0t/go-pretty/v6/text" 8 | 9 | "github.com/bartosian/suimon/internal/core/domain/enums" 10 | ) 11 | 12 | const suiEmoji = "💧" 13 | 14 | // tableStyleDefault defines the default style for the tables. 15 | // It includes the box style, color options, and other table options. 16 | // The box style defines the characters used for the table borders and separators. 17 | // The color options define the colors used for the header, rows, and footer. 18 | // The table options define various settings such as whether to draw borders and separate columns, rows, etc. 19 | var tableStyleDefault = table.Style{ 20 | Name: "DEFAULT", 21 | Box: table.BoxStyle{ 22 | BottomLeft: "└", 23 | BottomRight: "┘", 24 | BottomSeparator: "┴", 25 | EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")), 26 | Left: "│", 27 | LeftSeparator: "├", 28 | MiddleHorizontal: "─", 29 | MiddleSeparator: "┼", 30 | MiddleVertical: "│", 31 | PaddingLeft: " ", 32 | PaddingRight: " ", 33 | PageSeparator: "\n", 34 | Right: "│", 35 | RightSeparator: "┤", 36 | TopLeft: "┌", 37 | TopRight: "┐", 38 | TopSeparator: "┬", 39 | UnfinishedRow: " ≈", 40 | }, 41 | Color: table.ColorOptions{ 42 | Header: text.Colors{text.FgBlack, text.BgWhite}, 43 | Row: text.Colors{text.BgWhite}, 44 | Footer: text.Colors{text.BgHiBlue, text.FgBlack}, 45 | }, 46 | Options: table.Options{ 47 | DoNotColorBordersAndSeparators: true, 48 | DrawBorder: true, 49 | SeparateColumns: true, 50 | SeparateFooter: true, 51 | SeparateHeader: true, 52 | SeparateRows: true, 53 | }, 54 | Title: table.TitleOptions{ 55 | Align: text.AlignLeft, 56 | Colors: text.Colors{text.BgHiBlue, text.FgBlack}, 57 | }, 58 | } 59 | 60 | // Define the mapping of TableType enums to their corresponding ColumnsConfig. 61 | var columnsConfigMap = map[enums.TableType]ColumnsConfig{ 62 | enums.TableTypeRPC: ColumnsConfigRPC, 63 | enums.TableTypeValidator: ColumnsConfigValidator, 64 | enums.TableTypeNode: ColumnsConfigNode, 65 | enums.TableTypeGasPriceAndSubsidy: ColumnsConfigSystem, 66 | enums.TableTypeValidatorParams: ColumnsConfigSystem, 67 | enums.TableTypeValidatorsAtRisk: ColumnsConfigSystem, 68 | enums.TableTypeValidatorReports: ColumnsConfigSystem, 69 | enums.TableTypeActiveValidators: ColumnsConfigActiveValidator, 70 | enums.TableTypeReleases: ColumnsConfigRelease, 71 | enums.TableTypeProtocol: ColumnsConfigProtocol, 72 | } 73 | 74 | // Define the mapping of TableType enums to their corresponding RowsConfig. 75 | var rowsConfigMap = map[enums.TableType]RowsConfig{ 76 | enums.TableTypeRPC: RowsConfigRPC, 77 | enums.TableTypeValidator: RowsConfigValidator, 78 | enums.TableTypeGasPriceAndSubsidy: RowsConfigSystemState, 79 | enums.TableTypeValidatorParams: RowsConfigValidatorParams, 80 | enums.TableTypeValidatorsAtRisk: RowsConfigValidatorsAtRisk, 81 | enums.TableTypeValidatorReports: RowsConfigValidatorReports, 82 | enums.TableTypeNode: RowsConfigNode, 83 | enums.TableTypeActiveValidators: RowsActiveValidator, 84 | enums.TableTypeReleases: RowsRelease, 85 | enums.TableTypeProtocol: RowsConfigProtocol, 86 | } 87 | 88 | // Define the mapping of TableType enums to their corresponding text.Colors. 89 | var tableColorMap = map[enums.TableType]text.Colors{ 90 | enums.TableTypeRPC: {text.BgHiBlue, text.FgBlack}, 91 | enums.TableTypeValidator: {text.BgHiBlue, text.FgBlack}, 92 | enums.TableTypeValidatorsAtRisk: {text.BgHiBlue, text.FgBlack}, 93 | enums.TableTypeActiveValidators: {text.BgHiBlue, text.FgBlack}, 94 | enums.TableTypeProtocol: {text.BgHiBlue, text.FgBlack}, 95 | } 96 | 97 | // defaultTableColor defines the default color configuration. 98 | var defaultTableColor = text.Colors{text.BgHiGreen, text.FgBlack} 99 | 100 | // RowsConfig represents the configuration of rows in a table, each row being a slice of column names. 101 | type RowsConfig [][]enums.ColumnName 102 | 103 | // SortConfig represents a slice of sorting configurations for a table. 104 | type SortConfig []table.SortBy 105 | 106 | // TableConfig represents the overall configuration for a table. 107 | type TableConfig struct { 108 | Columns ColumnsConfig // Configuration for the table's columns 109 | Name string // The name of the table 110 | Rows RowsConfig // Configuration for the table's rows 111 | Style table.Style // The style of the table 112 | ColumnsCount int // The total number of columns in the table 113 | RowsCount int // The total number of rows in the table 114 | } 115 | 116 | // NewDefaultTableConfig returns a new default table configuration based on the specified table type. 117 | // It sets the table name, style, sort, rows, columns, column count, and auto-index. 118 | func NewDefaultTableConfig(domainTable enums.TableType) *TableConfig { 119 | tableName := fmt.Sprintf("%s [ %s ]", suiEmoji, domainTable) 120 | columnsConfig := GetColumnsConfig(domainTable) 121 | rowsConfig := GetRowsConfig(domainTable) 122 | tableColor := GetTableColor(domainTable) 123 | 124 | tableStyle := tableStyleDefault 125 | tableStyle.Title.Colors = tableColor 126 | tableStyle.Color.Footer = tableColor 127 | 128 | return &TableConfig{ 129 | Name: tableName, 130 | Style: tableStyle, 131 | Rows: rowsConfig, 132 | Columns: columnsConfig, 133 | ColumnsCount: len(columnsConfig), 134 | } 135 | } 136 | 137 | // GetColumnsConfig returns the ColumnsConfig based on the provided table type. 138 | func GetColumnsConfig(domainTable enums.TableType) ColumnsConfig { 139 | config, ok := columnsConfigMap[domainTable] 140 | if !ok { 141 | return nil 142 | } 143 | 144 | return config 145 | } 146 | 147 | // GetRowsConfig returns the rows configuration based on the specified table type. 148 | func GetRowsConfig(domainTable enums.TableType) RowsConfig { 149 | config, ok := rowsConfigMap[domainTable] 150 | if !ok { 151 | return nil 152 | } 153 | 154 | return config 155 | } 156 | 157 | // GetTableColor returns the color configuration based on the specified table type. 158 | func GetTableColor(domainTable enums.TableType) text.Colors { 159 | color, ok := tableColorMap[domainTable] 160 | if !ok { 161 | return defaultTableColor 162 | } 163 | 164 | return color 165 | } 166 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/node.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mum4k/termdash/cell" 7 | 8 | "github.com/bartosian/suimon/internal/core/domain/enums" 9 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 10 | ) 11 | 12 | var ( 13 | ColumnsConfigNode = ColumnsConfig{ 14 | // Overview section 15 | enums.ColumnNameCurrentEpoch: ColumnWidth33, 16 | enums.ColumnNameNetworkPeers: ColumnWidth15, 17 | enums.ColumnNameUptime: ColumnWidth25, 18 | enums.ColumnNameVersion: ColumnWidth25, 19 | enums.ColumnNameCommit: ColumnWidth25, 20 | enums.ColumnNameCheckpointExecBacklog: ColumnWidth33, 21 | enums.ColumnNameCheckpointSyncBacklog: ColumnWidth33, 22 | 23 | // Transactions section 24 | enums.ColumnNameTotalTransactionBlocks: ColumnWidth33, 25 | enums.ColumnNameTotalTransactionCertificates: ColumnWidth33, 26 | enums.ColumnNameTotalTransactionEffects: ColumnWidth33, 27 | enums.ColumnNameTXSyncPercentage: ColumnWidth49, 28 | enums.ColumnNameTransactionsPerSecond: ColumnWidth49, 29 | 30 | // Checkpoints section 31 | enums.ColumnNameLatestCheckpoint: ColumnWidth24, 32 | enums.ColumnNameHighestKnownCheckpoint: ColumnWidth24, 33 | enums.ColumnNameHighestSyncedCheckpoint: ColumnWidth24, 34 | enums.ColumnNameLastExecutedCheckpoint: ColumnWidth24, 35 | enums.ColumnNameCheckSyncPercentage: ColumnWidth49, 36 | enums.ColumnNameCheckpointsPerSecond: ColumnWidth49, 37 | } 38 | 39 | RowsConfigNode = RowsConfig{ 40 | 0: { 41 | Height: RowHeight14, 42 | Columns: []enums.ColumnName{ 43 | enums.ColumnNameNetworkPeers, 44 | enums.ColumnNameUptime, 45 | enums.ColumnNameVersion, 46 | enums.ColumnNameCommit, 47 | }, 48 | }, 49 | 1: { 50 | Height: RowHeight14, 51 | Columns: []enums.ColumnName{ 52 | enums.ColumnNameCurrentEpoch, 53 | enums.ColumnNameCheckpointExecBacklog, 54 | enums.ColumnNameCheckpointSyncBacklog, 55 | }, 56 | }, 57 | 2: { 58 | Height: RowHeight14, 59 | Columns: []enums.ColumnName{ 60 | enums.ColumnNameLatestCheckpoint, 61 | enums.ColumnNameHighestKnownCheckpoint, 62 | enums.ColumnNameHighestSyncedCheckpoint, 63 | enums.ColumnNameLastExecutedCheckpoint, 64 | }, 65 | }, 66 | 3: { 67 | Height: RowHeight14, 68 | Columns: []enums.ColumnName{ 69 | enums.ColumnNameCheckSyncPercentage, 70 | enums.ColumnNameCheckpointsPerSecond, 71 | }, 72 | }, 73 | 4: { 74 | Height: RowHeight14, 75 | Columns: []enums.ColumnName{ 76 | enums.ColumnNameTotalTransactionBlocks, 77 | enums.ColumnNameTotalTransactionCertificates, 78 | enums.ColumnNameTotalTransactionEffects, 79 | }, 80 | }, 81 | 5: { 82 | Height: RowHeight14, 83 | Columns: []enums.ColumnName{ 84 | enums.ColumnNameTXSyncPercentage, 85 | enums.ColumnNameTransactionsPerSecond, 86 | }, 87 | }, 88 | } 89 | 90 | CellsConfigNode = CellsConfig{ 91 | enums.ColumnNameNetworkPeers: {"NETWORK PEERS", cell.ColorGreen}, 92 | enums.ColumnNameUptime: {"UPTIME", cell.ColorGreen}, 93 | enums.ColumnNameVersion: {"VERSION", cell.ColorGreen}, 94 | enums.ColumnNameCommit: {"COMMIT", cell.ColorGreen}, 95 | enums.ColumnNameCurrentEpoch: {"CURRENT EPOCH", cell.ColorGreen}, 96 | enums.ColumnNameCheckpointExecBacklog: {"CHECKPOINT EXEC BACKLOG", cell.ColorGreen}, 97 | enums.ColumnNameCheckpointSyncBacklog: {"CHECKPOINT SYNC BACKLOG", cell.ColorGreen}, 98 | enums.ColumnNameHighestKnownCheckpoint: {"HIGHEST KNOWN CHECKPOINT", cell.ColorBlue}, 99 | enums.ColumnNameHighestSyncedCheckpoint: {"HIGHEST SYNCED CHECKPOINT", cell.ColorBlue}, 100 | enums.ColumnNameLastExecutedCheckpoint: {"LAST EXECUTED CHECKPOINT", cell.ColorBlue}, 101 | enums.ColumnNameLatestCheckpoint: {"LATEST CHECKPOINT", cell.ColorBlue}, 102 | enums.ColumnNameCheckSyncPercentage: {"CHECKPOINTS SYNC PERCENTAGE", cell.ColorBlue}, 103 | enums.ColumnNameCheckpointsPerSecond: {"CHECKPOINTS VOLUME", cell.ColorBlue}, 104 | enums.ColumnNameTotalTransactionBlocks: {"TOTAL TRANSACTION BLOCKS", cell.ColorYellow}, 105 | enums.ColumnNameTotalTransactionCertificates: {"TOTAL TRANSACTION CERTIFICATES", cell.ColorYellow}, 106 | enums.ColumnNameTotalTransactionEffects: {"TOTAL TRANSACTION EFFECTS", cell.ColorYellow}, 107 | enums.ColumnNameTransactionsPerSecond: {"TRANSACTIONS VOLUME", cell.ColorYellow}, 108 | enums.ColumnNameTXSyncPercentage: {"TRANSACTIONS SYNC PERCENTAGE", cell.ColorYellow}, 109 | } 110 | ) 111 | 112 | // GetNodeColumnValues returns a map of ColumnName values to corresponding values for a node at the specified index on the specified host. 113 | // The function retrieves information about the node from the host's internal state and formats it into a map of NodeColumnName keys and corresponding values. 114 | // The function also includes emoji values in the map if the specified flag is true. 115 | func GetNodeColumnValues(host *domainhost.Host) (ColumnValues, error) { 116 | return ColumnValues{ 117 | enums.ColumnNameTotalTransactionBlocks: host.Metrics.TotalTransactionsBlocks, 118 | enums.ColumnNameTotalTransactionCertificates: host.Metrics.TotalTransactionCertificates, 119 | enums.ColumnNameTotalTransactionEffects: host.Metrics.TotalTransactionEffects, 120 | enums.ColumnNameTransactionsPerSecond: host.Metrics.TransactionsPerSecond, 121 | enums.ColumnNameLatestCheckpoint: host.Metrics.LatestCheckpoint, 122 | enums.ColumnNameHighestKnownCheckpoint: host.Metrics.HighestKnownCheckpoint, 123 | enums.ColumnNameHighestSyncedCheckpoint: host.Metrics.HighestSyncedCheckpoint, 124 | enums.ColumnNameLastExecutedCheckpoint: host.Metrics.LastExecutedCheckpoint, 125 | enums.ColumnNameCheckpointExecBacklog: host.Metrics.CheckpointExecBacklog, 126 | enums.ColumnNameCheckpointSyncBacklog: host.Metrics.CheckpointSyncBacklog, 127 | enums.ColumnNameCurrentEpoch: host.Metrics.CurrentEpoch, 128 | enums.ColumnNameTXSyncPercentage: fmt.Sprintf("%v%%", host.Metrics.TxSyncPercentage), 129 | enums.ColumnNameCheckSyncPercentage: fmt.Sprintf("%v%%", host.Metrics.CheckSyncPercentage), 130 | enums.ColumnNameCheckpointsPerSecond: host.Metrics.CheckpointsPerSecond, 131 | enums.ColumnNameNetworkPeers: host.Metrics.NetworkPeers, 132 | enums.ColumnNameUptime: host.Metrics.Uptime, 133 | enums.ColumnNameVersion: host.Metrics.Version, 134 | enums.ColumnNameCommit: host.Metrics.Commit, 135 | }, nil 136 | } 137 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/base.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mum4k/termdash/align" 7 | "github.com/mum4k/termdash/cell" 8 | "github.com/mum4k/termdash/container" 9 | "github.com/mum4k/termdash/container/grid" 10 | "github.com/mum4k/termdash/linestyle" 11 | 12 | "github.com/bartosian/suimon/internal/core/domain/enums" 13 | domainhost "github.com/bartosian/suimon/internal/core/domain/host" 14 | ) 15 | 16 | const ( 17 | dashboardName = "💧 SUIMON: PRESS Q or ESC TO QUIT" 18 | emptyRowHeight = 1 19 | ) 20 | 21 | var DashboardConfigDefault = []container.Option{ 22 | container.Border(linestyle.Light), 23 | container.BorderColor(cell.ColorGreen), 24 | container.BorderTitle(dashboardName), 25 | container.FocusedColor(cell.ColorGreen), 26 | container.AlignHorizontal(align.HorizontalCenter), 27 | container.AlignVertical(align.VerticalMiddle), 28 | container.TitleColor(cell.ColorRed), 29 | container.TitleFocusedColor(cell.ColorRed), 30 | container.Focused(), 31 | } 32 | 33 | var CellConfigDefault = []container.Option{ 34 | container.Border(linestyle.Light), 35 | container.AlignVertical(align.VerticalMiddle), 36 | container.AlignHorizontal(align.HorizontalCenter), 37 | container.TitleColor(cell.ColorRed), 38 | container.FocusedColor(cell.ColorWhite), 39 | } 40 | 41 | // GetColumnsConfig returns the columns configuration based on the specified dashboard type. 42 | func GetColumnsConfig(dashboard enums.TableType) (ColumnsConfig, error) { 43 | configMap := map[enums.TableType]ColumnsConfig{ 44 | enums.TableTypeNode: ColumnsConfigNode, 45 | enums.TableTypeValidator: ColumnsConfigValidator, 46 | enums.TableTypeRPC: ColumnsConfigRPC, 47 | enums.TableTypeGasPriceAndSubsidy: ColumnsConfigSystemState, 48 | } 49 | 50 | config, ok := configMap[dashboard] 51 | if !ok { 52 | return nil, fmt.Errorf("unknown dashboard type: %v", dashboard) 53 | } 54 | 55 | return config, nil 56 | } 57 | 58 | // GetColumnsValues returns the columns values based on the specified dashboard type and host. 59 | func GetColumnsValues(dashboard enums.TableType, host *domainhost.Host) (ColumnValues, error) { 60 | columnsValuesFuncMap := map[enums.TableType]func(*domainhost.Host) (ColumnValues, error){ 61 | enums.TableTypeNode: GetNodeColumnValues, 62 | enums.TableTypeValidator: GetValidatorColumnValues, 63 | enums.TableTypeRPC: GetRPCColumnValues, 64 | enums.TableTypeGasPriceAndSubsidy: GeSystemStateColumnValues, 65 | } 66 | 67 | if columnValuesFunc, ok := columnsValuesFuncMap[dashboard]; ok { 68 | return columnValuesFunc(host) 69 | } 70 | 71 | return nil, fmt.Errorf("unknown dashboard type: %v", dashboard) 72 | } 73 | 74 | // GetRowsConfig returns the rows configuration based on the specified dashboard type. 75 | func GetRowsConfig(dashboard enums.TableType) (RowsConfig, error) { 76 | rowsConfigMap := map[enums.TableType]RowsConfig{ 77 | enums.TableTypeNode: RowsConfigNode, 78 | enums.TableTypeValidator: RowsConfigValidator, 79 | enums.TableTypeRPC: RowsConfigRPC, 80 | enums.TableTypeGasPriceAndSubsidy: RowsConfigSystemState, 81 | } 82 | 83 | config, ok := rowsConfigMap[dashboard] 84 | if !ok { 85 | return nil, fmt.Errorf("unknown dashboard type: %v", dashboard) 86 | } 87 | 88 | return config, nil 89 | } 90 | 91 | // GetCellsConfig returns the cells configuration based on the specified dashboard type. 92 | func GetCellsConfig(dashboard enums.TableType) (CellsConfig, error) { 93 | cellsConfigMap := map[enums.TableType]CellsConfig{ 94 | enums.TableTypeNode: CellsConfigNode, 95 | enums.TableTypeValidator: CellsConfigValidator, 96 | enums.TableTypeRPC: CellsConfigRPC, 97 | enums.TableTypeGasPriceAndSubsidy: CellsConfigSystemState, 98 | } 99 | 100 | config, ok := cellsConfigMap[dashboard] 101 | if !ok { 102 | return nil, fmt.Errorf("unknown dashboard type: %v", dashboard) 103 | } 104 | 105 | return config, nil 106 | } 107 | 108 | // GetColumns returns a slice of Columns based on the given ColumnsConfig and Cells. 109 | func GetColumns(columnsConfig ColumnsConfig, cells Cells) (Columns, error) { 110 | columns := make(Columns, len(columnsConfig)) 111 | 112 | for columnName, width := range columnsConfig { 113 | dashCell, ok := cells[columnName] 114 | if !ok { 115 | return nil, fmt.Errorf("failed to get cell for column %s", columnName) 116 | } 117 | 118 | columnPct := NewColumnPct(width, dashCell.GetWidget()) 119 | 120 | columns[columnName] = columnPct 121 | } 122 | 123 | return columns, nil 124 | } 125 | 126 | // GetRows creates a new set of rows based on the configuration provided. 127 | // It accepts a RowsConfig object that specifies the height and columns for each row, 128 | // a Cells object that maps cell names to cell objects, 129 | // and a Columns object that maps column names to column objects. 130 | // It returns a Rows object and an error. The Rows object is a slice of Row objects, 131 | // where each Row object represents a row in the terminal grid. 132 | func GetRows(rowsConfig RowsConfig, columns Columns) (Rows, error) { 133 | rows := make(Rows, 0, len(rowsConfig)) 134 | 135 | for _, rowConfig := range rowsConfig { 136 | rowColumns := make([]grid.Element, 0, len(rowConfig.Columns)) 137 | 138 | for _, columnName := range rowConfig.Columns { 139 | column, ok := columns[columnName] 140 | if !ok { 141 | return nil, fmt.Errorf("failed to get column %s", columnName) 142 | } 143 | 144 | rowColumns = append(rowColumns, column) 145 | } 146 | 147 | rows = append(rows, NewRowPct(rowConfig.Height, rowColumns...)) 148 | } 149 | 150 | // add empty row to limit last row height 151 | rows = append(rows, NewRowPct(emptyRowHeight)) 152 | 153 | return rows, nil 154 | } 155 | 156 | // GetCells creates a new set of cells based on the configuration provided. 157 | // It accepts a CellsConfig object that maps column names to cell names, 158 | // and a terminal object that represents the terminal where the cells will be displayed. 159 | // It returns a Cells object and an error. The Cells object is a map that maps 160 | // column names to cell objects. 161 | func GetCells(cellsConfig CellsConfig) (Cells, error) { 162 | cells := make(Cells, len(cellsConfig)) 163 | 164 | for columnName, cellConfig := range cellsConfig { 165 | widget, err := newWidgetByColumnName(columnName, cellConfig.Color) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | dashCell, err := NewCell(cellConfig.Title, cellConfig.Color, widget) 171 | if err != nil { 172 | return nil, fmt.Errorf("failed to create new cell for %s: %w", columnName, err) 173 | } 174 | 175 | cells[columnName] = dashCell 176 | } 177 | 178 | return cells, nil 179 | } 180 | -------------------------------------------------------------------------------- /internal/pkg/address/address.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "strings" 8 | 9 | externalIP "github.com/glendc/go-external-ip" 10 | 11 | "github.com/bartosian/suimon/internal/pkg/validation" 12 | ) 13 | 14 | type Endpoint struct { 15 | IP *string 16 | Host *string 17 | Path *string 18 | Port *string 19 | Address string 20 | SSL bool 21 | } 22 | 23 | const ( 24 | errInvalidPeerFormatProvided = "invalid peer format provided: %s" 25 | errInvalidPortProvided = "invalid port provided: %s" 26 | errInvalidIPProvided = "invalid ip provided: %s" 27 | errInvalidURLProvided = "invalid url provided: %s" 28 | ipProtocol4 = 4 29 | peerParts = 5 30 | ) 31 | 32 | // GetHostWithPath returns the host with the path, if available. 33 | // If the host is nil, it returns nil. Otherwise, it concatenates the host and path and returns the result. 34 | // Returns a pointer to the concatenated host with the path. 35 | func (hp *Endpoint) GetHostWithPath() *string { 36 | if hp.Host == nil { 37 | return nil 38 | } 39 | 40 | hostPath := *hp.Host 41 | 42 | if hp.Path != nil { 43 | hostPath += *hp.Path 44 | } 45 | 46 | return &hostPath 47 | } 48 | 49 | // ParseIpPort parses the given address and returns an Endpoint and an error. 50 | // If the address is in the format "ip:port", it returns the IP and port as an Endpoint. 51 | // If the address is in the format "protocol/ip4/host/udp/port", it returns the host and port as an Endpoint. 52 | // If the IP provided is a loopback or unspecified IP, it replaces it with the public IP. 53 | // Returns the parsed Endpoint and nil error if successful, otherwise returns nil and an error. 54 | func ParseIPPort(address string) (*Endpoint, error) { 55 | ip, port, err := net.SplitHostPort(address) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | if validation.IsInvalidPort(port) { 61 | return nil, fmt.Errorf(errInvalidPortProvided, address) 62 | } 63 | 64 | if parsedIP := net.ParseIP(ip); parsedIP.IsLoopback() || parsedIP.IsUnspecified() { 65 | ip = GetPublicIP().String() 66 | address = fmt.Sprintf("%s:%s", ip, port) 67 | } 68 | 69 | return &Endpoint{ 70 | Address: address, 71 | IP: &ip, 72 | Port: &port, 73 | }, nil 74 | } 75 | 76 | // ParsePeer parses the given address and returns an Endpoint and an error. 77 | // If the address is in the format "/ip4/host/udp/port", it returns the host and port as an Endpoint. 78 | // If the IP provided is a loopback or unspecified IP, it replaces it with the public IP. 79 | // Returns the parsed Endpoint and nil error if successful, otherwise returns nil and an error. 80 | func ParsePeer(address string) (*Endpoint, error) { 81 | components := strings.Split(address, "/") 82 | 83 | if len(components) != peerParts { 84 | return nil, fmt.Errorf(errInvalidPeerFormatProvided, address) 85 | } 86 | 87 | validProtocol := components[3] == "udp" 88 | validFirstComponent := components[0] == "" 89 | validSecondComponent := components[1] == "ip4" || components[1] == "dns" 90 | 91 | if !validProtocol || !validFirstComponent || !validSecondComponent { 92 | return nil, fmt.Errorf(errInvalidPeerFormatProvided, address) 93 | } 94 | 95 | host, port := components[2], components[4] 96 | 97 | if validation.IsInvalidPort(port) { 98 | return nil, fmt.Errorf(errInvalidPortProvided, address) 99 | } 100 | 101 | endpoint := &Endpoint{ 102 | Address: address, 103 | Port: &port, 104 | } 105 | 106 | if ip, err := ParseIP(host); err != nil { 107 | endpoint.Host = &host 108 | } else { 109 | endpoint.IP = ip 110 | } 111 | 112 | return endpoint, nil 113 | } 114 | 115 | // ParseURL parses the given address and returns an Endpoint and an error. 116 | // If the address does not start with "http", it adds "http://" to the beginning of the address. 117 | // It then parses the address using the url.Parse function and constructs an Endpoint with the parsed information. 118 | // Returns the parsed Endpoint and nil error if successful, otherwise returns nil and an error. 119 | func ParseURL(address string) (*Endpoint, error) { 120 | if !strings.HasPrefix(address, "http") { 121 | address = "http://" + address 122 | } 123 | 124 | u, err := url.Parse(address) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | scheme, hostName, port, path := u.Scheme, u.Hostname(), u.Port(), u.Path 130 | 131 | if hostName == "" { 132 | return nil, fmt.Errorf(errInvalidURLProvided, address) 133 | } 134 | 135 | fullAddress := fmt.Sprintf("%s://%s%s", scheme, hostName, path) 136 | if port != "" { 137 | fullAddress = fmt.Sprintf("%s://%s:%s%s", scheme, hostName, port, path) 138 | } 139 | 140 | endpoint := &Endpoint{ 141 | Address: fullAddress, 142 | Host: &hostName, 143 | SSL: scheme == "https", 144 | } 145 | 146 | if ip, parseErr := ParseIP(hostName); parseErr == nil { 147 | endpoint.IP = ip 148 | } 149 | 150 | if port != "" { 151 | if validation.IsInvalidPort(port) { 152 | return nil, fmt.Errorf(errInvalidPortProvided, address) 153 | } 154 | 155 | endpoint.Port = &port 156 | } 157 | 158 | if path != "" { 159 | endpoint.Path = &path 160 | } 161 | 162 | if ip, getErr := GetIPByDomain(address); getErr == nil { 163 | endpoint.IP = ip 164 | } 165 | 166 | return endpoint, nil 167 | } 168 | 169 | // GetIPByDomain performs a DNS lookup to retrieve the IP address associated with the provided domain. 170 | // It takes the domain name as input and returns the IP address and nil error if successful, otherwise returns nil and an error. 171 | func GetIPByDomain(address string) (*string, error) { 172 | ips, err := net.LookupIP(address) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | ip := ips[0].String() 178 | 179 | return &ip, nil 180 | } 181 | 182 | // ParseIP parses the given IP address and returns the parsed IP and nil error if successful, otherwise returns nil and an error. 183 | // If the provided IP is a loopback or unspecified IP, it replaces it with the public IP. 184 | func ParseIP(address string) (*string, error) { 185 | if ip := net.ParseIP(address); ip != nil { 186 | if ip.IsLoopback() { 187 | ip = GetPublicIP() 188 | } 189 | 190 | ipResult := ip.String() 191 | 192 | return &ipResult, nil 193 | } 194 | 195 | return nil, fmt.Errorf(errInvalidIPProvided, address) 196 | } 197 | 198 | // GetPublicIP retrieves the public IP address using the default consensus and returns it. 199 | // It returns the public IP address if successful, otherwise returns nil. 200 | func GetPublicIP() net.IP { 201 | consensus := externalIP.DefaultConsensus(nil, nil) 202 | if err := consensus.UseIPProtocol(ipProtocol4); err != nil { 203 | return nil 204 | } 205 | 206 | ip, err := consensus.ExternalIP() 207 | if err != nil { 208 | return nil 209 | } 210 | 211 | return ip 212 | } 213 | -------------------------------------------------------------------------------- /internal/core/domain/service/dashboardbuilder/dashboards/cell.go: -------------------------------------------------------------------------------- 1 | package dashboards 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/mum4k/termdash/cell" 10 | "github.com/mum4k/termdash/container" 11 | "github.com/mum4k/termdash/container/grid" 12 | "github.com/mum4k/termdash/widgetapi" 13 | "github.com/mum4k/termdash/widgets/gauge" 14 | "github.com/mum4k/termdash/widgets/segmentdisplay" 15 | "github.com/mum4k/termdash/widgets/sparkline" 16 | "github.com/mum4k/termdash/widgets/text" 17 | 18 | "github.com/bartosian/suimon/internal/core/domain/enums" 19 | "github.com/bartosian/suimon/internal/pkg/log" 20 | "github.com/bartosian/suimon/internal/pkg/utility" 21 | ) 22 | 23 | // CellsConfig is a type that represents a mapping of column names to CellConfig. 24 | type ( 25 | CellsConfig map[enums.ColumnName]CellConfig 26 | CellConfig struct { 27 | Title string 28 | Color cell.Color 29 | } 30 | ) 31 | 32 | // Cells is a type that represents a mapping of column names to pointers to Cell structs. 33 | type Cells map[enums.ColumnName]*Cell 34 | 35 | // Cell is a struct that represents a single cell in a dashboard grid. It contains a widget and a list of options. 36 | type Cell struct { 37 | LastUpdatedAt time.Time 38 | Widget widgetapi.Widget 39 | Options []container.Option 40 | } 41 | 42 | // NewCell is a function that creates a new Cell struct given a cellName and a widget. It returns a pointer to the new Cell and an error (if any). 43 | func NewCell(cellName string, color cell.Color, widget widgetapi.Widget) (*Cell, error) { 44 | CellConfigDefault = append(CellConfigDefault, container.BorderTitle(cellName), container.BorderColor(color)) 45 | 46 | dashCell := Cell{ 47 | Widget: widget, 48 | Options: CellConfigDefault, 49 | LastUpdatedAt: time.Now(), 50 | } 51 | 52 | return &dashCell, nil 53 | } 54 | 55 | // GetWidget is a method attached to the Cell struct that returns a widget that can be added to a dashboard grid. 56 | func (c Cell) GetWidget() grid.Element { 57 | return grid.Widget(c.Widget, c.Options...) 58 | } 59 | 60 | // Write writes a value to the cell widget. 61 | // It accepts a value to write. 62 | // The type of value must match the type expected by the cell widget. 63 | // If the widget type is not recognized, the function returns nil. 64 | func (c *Cell) Write(value any) error { 65 | now := time.Now() 66 | 67 | switch widget := c.Widget.(type) { 68 | case *gauge.Gauge: 69 | return writeToGaugeWidget(widget, value) 70 | case *text.Text: 71 | return writeToTextWidget(widget, value) 72 | case *segmentdisplay.SegmentDisplay: 73 | return writeToSegmentWidget(widget, value) 74 | case *sparkline.SparkLine: 75 | if now.Sub(c.LastUpdatedAt) < 2*time.Second { 76 | return nil 77 | } 78 | 79 | c.LastUpdatedAt = now 80 | 81 | return writeToSparkLineWidget(widget, value) 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // writeToTextWidget writes a string value to a text widget with the given options. 88 | // The function expects a slice of cell options and a value of type string, 89 | // and returns an error if the value has a different type. The function uses 90 | // the `text.Text` type and its `Write` method to write the string value to the widget, 91 | // with the options converted to `text.WriteOption` using the `text.WriteCellOpts` function. 92 | // The function removes any non-printable characters from the string value before writing it 93 | // to the widget, and returns immediately if the resulting string has zero length. 94 | func writeToTextWidget(widget *text.Text, value any) error { 95 | valueString, ok := value.(string) 96 | if !ok { 97 | return fmt.Errorf("invalid value type for text widget: %T", value) 98 | } 99 | 100 | valueString = log.RemoveNonPrintableChars(valueString) 101 | if valueString == "" { 102 | return nil 103 | } 104 | 105 | return widget.Write(valueString) 106 | } 107 | 108 | // writeToGaugeWidget writes a value to a gauge widget. 109 | // It accepts a gauge widget and a value to write. 110 | // The value must be a string representing an integer, or an error will be returned. 111 | func writeToGaugeWidget(widget *gauge.Gauge, value any) error { 112 | valueString, ok := value.(string) 113 | if !ok { 114 | return fmt.Errorf("unexpected metric value type for gauge widget: %T", value) 115 | } 116 | 117 | valueInt, err := utility.ParseIntFromString(valueString) 118 | if err != nil { 119 | return fmt.Errorf("unexpected metric value type for gauge widget: %T", value) 120 | } 121 | 122 | return widget.Percent(valueInt) 123 | } 124 | 125 | // writeToSparkLineWidget adds a new value to a sparkline chart widget. 126 | // The function expects an integer value and returns an error if the value 127 | // has a different type. It uses the `widget` argument to add the new value 128 | // to the chart using the `Add` method of the `sparkline.SparkLine` type. 129 | func writeToSparkLineWidget(widget *sparkline.SparkLine, value any) error { 130 | valueInt, ok := value.(int) 131 | if !ok { 132 | return fmt.Errorf("unexpected metric value type for sparkline widget: %T", value) 133 | } 134 | 135 | return widget.Add([]int{valueInt}) 136 | } 137 | 138 | // writeToSegmentWidget writes a value to a segment display widget. 139 | // It accepts a segment display widget and a value to write. 140 | // The value can be an integer, a string, or a slice of strings. 141 | // If the value is a string and it is empty, a blinking value will be used. 142 | // If a string in the slice is empty, a blinking value will be used for that chunk. 143 | func writeToSegmentWidget(widget *segmentdisplay.SegmentDisplay, value any) error { 144 | capacity := widget.Capacity() 145 | 146 | var chunks []*segmentdisplay.TextChunk 147 | 148 | switch v := value.(type) { 149 | case int64: 150 | chunk := strconv.FormatInt(v, 10) 151 | 152 | chunks = append(chunks, segmentdisplay.NewChunk(chunk)) 153 | case int: 154 | chunk := strconv.Itoa(v) 155 | 156 | chunks = append(chunks, segmentdisplay.NewChunk(chunk)) 157 | case string: 158 | chunk := v 159 | 160 | if chunk == "" { 161 | chunk = dashboardLoadingBlinkValue(capacity) 162 | } 163 | 164 | chunks = append(chunks, segmentdisplay.NewChunk(chunk)) 165 | case []string: 166 | for _, chunk := range v { 167 | if chunk == "" { 168 | chunk = dashboardLoadingBlinkValue(capacity) 169 | } 170 | 171 | chunks = append(chunks, segmentdisplay.NewChunk(chunk)) 172 | } 173 | } 174 | 175 | return widget.Write(chunks) 176 | } 177 | 178 | // dashboardLoadingBlinkValue returns a string that represents a loading 179 | // animation that blinks every even second. 180 | // 181 | // The animation consists of a series of "-" characters that fill up the 182 | // capacity parameter, which represents the maximum length of the animation. 183 | // Every even second, the animation is replaced with spaces to create a 184 | // blinking effect. 185 | func dashboardLoadingBlinkValue(maxLength int) string { 186 | inProgress := strings.Repeat("-", maxLength) 187 | second := time.Now().Second() 188 | 189 | if second%2 == 0 { 190 | inProgress = strings.Repeat("\u0020", maxLength) 191 | } 192 | 193 | return inProgress 194 | } 195 | -------------------------------------------------------------------------------- /internal/core/controllers/monitor/getaddressinfo.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/bartosian/suimon/internal/core/domain/enums" 8 | "github.com/bartosian/suimon/internal/core/domain/host" 9 | "github.com/bartosian/suimon/internal/pkg/address" 10 | ) 11 | 12 | type addressParser func(string) (*address.Endpoint, error) 13 | 14 | var parserMap = map[enums.TableType]addressParser{ 15 | enums.TableTypeNode: address.ParseURL, 16 | enums.TableTypeValidator: address.ParseURL, 17 | enums.TableTypeRPC: address.ParseURL, 18 | } 19 | 20 | // getAddressInfoByTableType retrieves the list of addresses for hosts that support the specified table type from the CheckerController's internal state. 21 | // The function returns an error if the specified table type is invalid or if there are no hosts that support the specified table type. 22 | // Returns a slice of AddressInfo structs and an error if the specified table type is invalid or if there are no hosts that support the specified table type. 23 | func (c *Controller) getAddressInfoByTableType(table enums.TableType) ([]host.AddressInfo, error) { 24 | addressFuncMap := map[enums.TableType]func(parser addressParser) ([]host.AddressInfo, error){ 25 | enums.TableTypeNode: c.getNodeAddresses, 26 | enums.TableTypeValidator: c.getValidatorAddresses, 27 | enums.TableTypeRPC: c.getRPCAddresses, 28 | } 29 | 30 | parser, parserExists := parserMap[table] 31 | if !parserExists { 32 | return nil, fmt.Errorf("invalid table type: %v", table) 33 | } 34 | 35 | if addressFunc, existsFunc := addressFuncMap[table]; existsFunc { 36 | addresses, err := addressFunc(parser) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return addresses, nil 42 | } 43 | 44 | return nil, fmt.Errorf("address function not found for table type: %v", table) 45 | } 46 | 47 | // getNodeAddresses extracts the JSON-RPC and metrics addresses from the selected config's full nodes and 48 | // returns an array of host.AddressInfo structs that include the endpoints and port numbers. 49 | // The parser argument is a function used to parse the address strings. 50 | // Returns an error if there is an invalid address format or if there is no JSON-RPC or metrics address provided for a full node. 51 | func (c *Controller) getNodeAddresses(parser addressParser) (addresses []host.AddressInfo, err error) { 52 | nodesConfig := c.selectedConfig.FullNodes 53 | if len(nodesConfig) == 0 { 54 | return []host.AddressInfo{}, nil 55 | } 56 | 57 | for _, node := range nodesConfig { 58 | addressRPC, addressMetrics := node.JSONRPCAddress, node.MetricsAddress 59 | 60 | if addressRPC == "" && addressMetrics == "" { 61 | return nil, errors.New("invalid format for full-node in dashboards file: at least one of json-rpc-address or metrics-address is required") 62 | } 63 | 64 | var addressInfo host.AddressInfo 65 | 66 | if addressRPC != "" { 67 | endpointRPC, parseErr := parser(addressRPC) 68 | if parseErr != nil { 69 | return nil, fmt.Errorf("invalid format for full-node json-rpc-address in config file: %w", parseErr) 70 | } 71 | 72 | addressInfo = host.AddressInfo{ 73 | Endpoint: *endpointRPC, 74 | Ports: map[enums.PortType]string{}, 75 | } 76 | 77 | if endpointRPC.Port != nil { 78 | addressInfo.Ports[enums.PortTypeRPC] = *endpointRPC.Port 79 | } 80 | } 81 | 82 | if addressMetrics != "" { 83 | endpointMetrics, parseErr := parser(addressMetrics) 84 | if parseErr != nil { 85 | return nil, fmt.Errorf("invalid format for full-node metrics-address in config file: %w", parseErr) 86 | } 87 | 88 | // If addressInfo is still empty, initialize it with endpointMetrics 89 | if addressInfo.Endpoint.Address == "" { 90 | addressInfo.Endpoint = *endpointMetrics 91 | addressInfo.Ports = map[enums.PortType]string{} 92 | } 93 | 94 | if endpointMetrics.Port != nil { 95 | addressInfo.Ports[enums.PortTypeMetrics] = *endpointMetrics.Port 96 | } 97 | } 98 | 99 | addresses = append(addresses, addressInfo) 100 | } 101 | 102 | return addresses, nil 103 | } 104 | 105 | // getValidatorAddresses returns the list of validator addresses. 106 | // It takes an addressParser as input and returns a list of host.AddressInfo and an error. 107 | // It processes the validator addresses and initializes the hosts. 108 | // If the validatorsConfig is empty, it returns an empty list. 109 | // If the metrics-address is missing for any validator, it returns an error. 110 | // If there is an error in parsing the validator metrics-address, it returns an error. 111 | // The function appends the processed addresses to the list and returns it along with any encountered error. 112 | func (c *Controller) getValidatorAddresses(parser addressParser) (addresses []host.AddressInfo, err error) { 113 | validatorsConfig := c.selectedConfig.Validators 114 | if len(validatorsConfig) == 0 { 115 | return []host.AddressInfo{}, nil 116 | } 117 | 118 | for _, validator := range validatorsConfig { 119 | addressMetrics := validator.MetricsAddress 120 | 121 | if addressMetrics == "" { 122 | return nil, errors.New("invalid format for validator in dashboards file: metrics-address is required") 123 | } 124 | 125 | endpointMetrics, parseErr := parser(addressMetrics) 126 | if parseErr != nil { 127 | return nil, fmt.Errorf("invalid format for validator metrics-address in config file: %w", parseErr) 128 | } 129 | 130 | addressInfo := host.AddressInfo{Endpoint: *endpointMetrics, Ports: make(map[enums.PortType]string)} 131 | 132 | if endpointMetrics.Port != nil { 133 | addressInfo.Ports[enums.PortTypeMetrics] = *endpointMetrics.Port 134 | } 135 | 136 | addresses = append(addresses, addressInfo) 137 | } 138 | 139 | return addresses, nil 140 | } 141 | 142 | // getRPCAddresses returns the list of reference RPC addresses. 143 | // It takes an addressParser as input and returns a list of host.AddressInfo and an error. 144 | // It processes the RPC addresses and initializes the hosts. 145 | // If the referenceRPCConfig is empty, it returns an error. 146 | // If the rpc-address is missing for any reference RPC, it returns an error. 147 | // If there is an error in parsing the reference RPC address, it returns an error. 148 | // The function appends the processed addresses to the list and returns it along with any encountered error. 149 | // This function is part of the Controller struct. 150 | func (c *Controller) getRPCAddresses(parser addressParser) (addresses []host.AddressInfo, err error) { 151 | rpcConfig := c.selectedConfig.ReferenceRPC 152 | if len(rpcConfig) == 0 { 153 | return nil, errors.New("reference-rpc not provided in config file") 154 | } 155 | 156 | for _, rpc := range rpcConfig { 157 | endpoint, parseErr := parser(rpc) 158 | if parseErr != nil { 159 | return nil, fmt.Errorf("invalid format for reference-rpc in config file: %w", parseErr) 160 | } 161 | 162 | addressInfo := host.AddressInfo{Endpoint: *endpoint, Ports: make(map[enums.PortType]string)} 163 | if endpoint.Port != nil { 164 | addressInfo.Ports[enums.PortTypeRPC] = *endpoint.Port 165 | } 166 | 167 | addresses = append(addresses, addressInfo) 168 | } 169 | 170 | return addresses, nil 171 | } 172 | --------------------------------------------------------------------------------