From de9da46b6b0ec9e1eae795cb73f0ab902cb87b08 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Thu, 14 Nov 2024 15:21:28 +0200 Subject: [PATCH] added comprehensive-vote-account-tracking --- README.md | 117 +++++++++++++++----------- cmd/solana-exporter/collector.go | 81 ++++++++++++++++-- cmd/solana-exporter/collector_test.go | 34 ++++++-- cmd/solana-exporter/config.go | 77 ++++++++++------- cmd/solana-exporter/config_test.go | 101 +++++++++++----------- pkg/rpc/mock.go | 3 +- pkg/rpc/mock_test.go | 12 +-- 7 files changed, 277 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 12d83d3..241df31 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,18 @@ CGO_ENABLED=0 go build ./cmd/solana-exporter The exporter is configured via the following command line arguments: -| Option | Description | Default | -|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| -| `-balance-address` | Address to monitor SOL balances for, in addition to the identity and vote accounts of the provided nodekeys - can be set multiple times. | N/A | -| `-comprehensive-slot-tracking` | Set this flag to track `solana_leader_slots_by_epoch` for all validators. | `false` | -| `-http-timeout` | HTTP timeout to use, in seconds. | `60` | -| `-light-mode` | Set this flag to enable light-mode. In light mode, only metrics unique to the node being queried are reported (i.e., metrics such as `solana_inflation_rewards` which are visible from any RPC node, are not reported). | `false` | -| `-listen-address` | Prometheus listen address. | `":8080"` | -| `-monitor-block-sizes` | Set this flag to track block sizes (number of transactions) for the configured validators. | `false` | -| `-nodekey` | Solana nodekey (identity account) representing a validator to monitor - can set multiple times. | N/A | -| `-rpc-url` | Solana RPC URL (including protocol and path), e.g., `"http://localhost:8899"` or `"https://api.mainnet-beta.solana.com"` | `"http://localhost:8899"` | -| `-slot-pace` | This is the time (in seconds) between slot-watching metric collections | `1` | +| Option | Description | Default | +|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| `-balance-address` | Address to monitor SOL balances for, in addition to the identity and vote accounts of the provided nodekeys - can be set multiple times. | N/A | +| `-comprehensive-slot-tracking` | Set this flag to track `solana_leader_slots_by_epoch` for all validators. | `false` | +| `-comprehensive-vote-account-tracking` | Set this flag to track vote-account metrics for all validators. | `false` | +| `-http-timeout` | HTTP timeout to use, in seconds. | `60` | +| `-light-mode` | Set this flag to enable light-mode. In light mode, only metrics unique to the node being queried are reported (i.e., metrics such as `solana_inflation_rewards` which are visible from any RPC node, are not reported). | `false` | +| `-listen-address` | Prometheus listen address. | `":8080"` | +| `-monitor-block-sizes` | Set this flag to track block sizes (number of transactions) for the configured validators. | `false` | +| `-nodekey` | Solana nodekey (identity account) representing a validator to monitor - can set multiple times. | N/A | +| `-rpc-url` | Solana RPC URL (including protocol and path), e.g., `"http://localhost:8899"` or `"https://api.mainnet-beta.solana.com"` | `"http://localhost:8899"` | +| `-slot-pace` | This is the time (in seconds) between slot-watching metric collections | `1` | ### Notes on Configuration @@ -61,45 +62,65 @@ The exporter is configured via the following command line arguments: ## Metrics ### Overview -The table below describes all the metrics collected by the `solana-exporter`: - -| Metric | Description | Labels | -|------------------------------------------------|------------------------------------------------------------------------------------------|-------------------------------| -| `solana_validator_active_stake` | Active stake per validator. | `votekey`, `nodekey` | -| `solana_validator_last_vote` | Last voted-on slot per validator. | `votekey`, `nodekey` | -| `solana_validator_root_slot` | Root slot per validator. | `votekey`, `nodekey` | -| `solana_validator_delinquent` | Whether a validator is delinquent. | `votekey`, `nodekey` | -| `solana_account_balance` | Solana account balances. | `address` | -| `solana_node_version` | Node version of solana.* | `version` | -| `solana_node_is_healthy` | Whether the node is healthy.* | N/A | -| `solana_node_num_slots_behind` | The number of slots that the node is behind the latest cluster confirmed slot.* | N/A | -| `solana_node_minimum_ledger_slot` | The lowest slot that the node has information about in its ledger.* | N/A | -| `solana_node_first_available_block` | The slot of the lowest confirmed block that has not been purged from the node's ledger.* | N/A | -| `solana_node_transactions_total` | Total number of transactions processed without error since genesis.* | N/A | -| `solana_node_slot_height` | The current slot number.* | N/A | -| `solana_node_epoch_number` | The current epoch number.* | N/A | -| `solana_node_epoch_first_slot` | Current epoch's first slot \[inclusive\].* | N/A | -| `solana_node_epoch_last_slot` | Current epoch's last slot \[inclusive\].* | N/A | -| `solana_validator_leader_slots_total` | Number of slots processed. | `status`, `nodekey` | -| `solana_validator_leader_slots_by_epoch_total` | Number of slots processed per validator. | `status`, `nodekey`, `epoch` | -| `solana_cluster_slots_by_epoch_total` | Number of slots processed by the cluster. | `status`, `epoch` | -| `solana_validator_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` | -| `solana_validator_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` | -| `solana_validator_block_size` | Number of transactions per block. | `nodekey`, `transaction_type` | -| `solana_node_block_height` | The current block height of the node.* | N/A | - -***NOTE***: An `*` in the description indicates that the metric **is** tracked in `-light-mode`. +The tables below describes all the metrics collected by the `solana-exporter`: + +| Metric | Description | Labels | +|------------------------------------------------|-----------------------------------------------------------------------------------------|-------------------------------| +| `solana_validator_active_stake` | Active stake (in SOL) per validator. | `votekey`, `nodekey` | +| `solana_cluster_active_stake` | Total active stake (in SOL) of the cluster. | N/A | +| `solana_validator_last_vote` | Last voted-on slot per validator. | `votekey`, `nodekey` | +| `solana_cluster_last_vote` | Most recent voted-on slot of the cluster. | N/A | +| `solana_validator_root_slot` | Root slot per validator. | `votekey`, `nodekey` | +| `solana_cluster_root_slot` | Max root slot of the cluster. | N/A | +| `solana_validator_delinquent` | Whether a validator is delinquent. | `votekey`, `nodekey` | +| `solana_cluster_validator_count` | Total number of validators in the cluster. | `state` | +| `solana_account_balance` | Solana account balances. | `address` | +| `solana_node_version` | Node version of solana. | `version` | +| `solana_node_is_healthy` | Whether the node is healthy. | N/A | +| `solana_node_num_slots_behind` | The number of slots that the node is behind the latest cluster confirmed slot. | N/A | +| `solana_node_minimum_ledger_slot` | The lowest slot that the node has information about in its ledger. | N/A | +| `solana_node_first_available_block` | The slot of the lowest confirmed block that has not been purged from the node's ledger. | N/A | +| `solana_node_transactions_total` | Total number of transactions processed without error since genesis. | N/A | +| `solana_node_slot_height` | The current slot number. | N/A | +| `solana_node_epoch_number` | The current epoch number. | N/A | +| `solana_node_epoch_first_slot` | Current epoch's first slot \[inclusive\]. | N/A | +| `solana_node_epoch_last_slot` | Current epoch's last slot \[inclusive\]. | N/A | +| `solana_validator_leader_slots_total` | Number of slots processed. | `status`, `nodekey` | +| `solana_validator_leader_slots_by_epoch_total` | Number of slots processed per validator. | `status`, `nodekey`, `epoch` | +| `solana_cluster_slots_by_epoch_total` | Number of slots processed by the cluster. | `status`, `epoch` | +| `solana_validator_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` | +| `solana_validator_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` | +| `solana_validator_block_size` | Number of transactions per block. | `nodekey`, `transaction_type` | +| `solana_node_block_height` | The current block height of the node. | N/A | + +#### Light Mode + +In `-light-mode`, the exporter will only track metrics that uniquely to the node being queried. These metric names +all begin with `solana_node_*`. + +#### Vote Account Metrics + +The following metrics are all received from the `getVoteAccounts` [RPC endpoint](https://solana.com/docs/rpc/http/getvoteaccounts): +* `solana_validator_active_stake` +* `solana_validator_last_vote` +* `solana_validator_root_slot` +* `solana_validator_delinquent` + +***NOTE***: If `-comprehensive-vote-account-tracking` is configured, then these metrics are tracked for **all** +validators. Regardless of comprehensive tracking, the above metrics' cluster counterparts are always tracked for easy +cluster-level comparison. ### Labels The table below describes the various metric labels: -| Label | Description | Options / Example | -|--------------------|-------------------------------------|------------------------------------------------------| -| `nodekey` | Validator identity account address. | e.g, `Certusm1sa411sMpV9FPqU5dXAYhmmhygvxJ23S6hJ24` | -| `votekey` | Validator vote account address. | e.g., `CertusDeBmqN8ZawdkxK5kFGMwBXdudvWHYwtNgNhvLu` | -| `address` | Solana account address. | e.g., `Certusm1sa411sMpV9FPqU5dXAYhmmhygvxJ23S6hJ24` | -| `version` | Solana node version. | e.g., `v1.18.23` | -| `status` | Whether a slot was skipped or valid | `valid`, `skipped` | -| `epoch` | Solana epoch number. | e.g., `663` | -| `transaction_type` | General transaction type. | `vote`, `non_vote` | +| Label | Description | Options / Example | +|--------------------|-----------------------------------------------|------------------------------------------------------| +| `nodekey` | Validator identity account address. | e.g, `Certusm1sa411sMpV9FPqU5dXAYhmmhygvxJ23S6hJ24` | +| `votekey` | Validator vote account address. | e.g., `CertusDeBmqN8ZawdkxK5kFGMwBXdudvWHYwtNgNhvLu` | +| `address` | Solana account address. | e.g., `Certusm1sa411sMpV9FPqU5dXAYhmmhygvxJ23S6hJ24` | +| `version` | Solana node version. | e.g., `v1.18.23` | +| `state` | Whether a validator is current or delinquent. | `current`, `delinquent` | +| `status` | Whether a slot was skipped or valid. | `valid`, `skipped` | +| `epoch` | Solana epoch number. | e.g., `663` | +| `transaction_type` | General transaction type. | `vote`, `non_vote` | diff --git a/cmd/solana-exporter/collector.go b/cmd/solana-exporter/collector.go index 2eddcbd..e42d10d 100644 --- a/cmd/solana-exporter/collector.go +++ b/cmd/solana-exporter/collector.go @@ -8,10 +8,12 @@ import ( "github.com/asymmetric-research/solana-exporter/pkg/slog" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" + "slices" ) const ( SkipStatusLabel = "status" + StateLabel = "state" NodekeyLabel = "nodekey" VotekeyLabel = "votekey" VersionLabel = "version" @@ -22,6 +24,9 @@ const ( StatusSkipped = "skipped" StatusValid = "valid" + StateCurrent = "current" + StateDelinquent = "delinquent" + TransactionTypeVote = "vote" TransactionTypeNonVote = "non_vote" ) @@ -34,9 +39,13 @@ type SolanaCollector struct { /// descriptors: ValidatorActiveStake *GaugeDesc + ClusterActiveStake *GaugeDesc ValidatorLastVote *GaugeDesc + ClusterLastVote *GaugeDesc ValidatorRootSlot *GaugeDesc + ClusterRootSlot *GaugeDesc ValidatorDelinquent *GaugeDesc + ClusterValidatorCount *GaugeDesc AccountBalances *GaugeDesc NodeVersion *GaugeDesc NodeIsHealthy *GaugeDesc @@ -55,21 +64,41 @@ func NewSolanaCollector(client *rpc.Client, config *ExporterConfig) *SolanaColle fmt.Sprintf("Active stake (in SOL) per validator (represented by %s and %s)", VotekeyLabel, NodekeyLabel), VotekeyLabel, NodekeyLabel, ), + ClusterActiveStake: NewGaugeDesc( + "solana_cluster_active_stake", + "Total active stake (in SOL) of the cluster", + ), ValidatorLastVote: NewGaugeDesc( "solana_validator_last_vote", fmt.Sprintf("Last voted-on slot per validator (represented by %s and %s)", VotekeyLabel, NodekeyLabel), VotekeyLabel, NodekeyLabel, ), + ClusterLastVote: NewGaugeDesc( + "solana_cluster_last_vote", + "Most recent voted-on slot of the cluster", + ), ValidatorRootSlot: NewGaugeDesc( "solana_validator_root_slot", fmt.Sprintf("Root slot per validator (represented by %s and %s)", VotekeyLabel, NodekeyLabel), VotekeyLabel, NodekeyLabel, ), + ClusterRootSlot: NewGaugeDesc( + "solana_cluster_root_slot", + "Max root slot of the cluster", + ), ValidatorDelinquent: NewGaugeDesc( "solana_validator_delinquent", fmt.Sprintf("Whether a validator (represented by %s and %s) is delinquent", VotekeyLabel, NodekeyLabel), VotekeyLabel, NodekeyLabel, ), + ClusterValidatorCount: NewGaugeDesc( + "solana_cluster_validator_count", + fmt.Sprintf( + "Total number of validators in the cluster, grouped by %s ('%s' or '%s')", + StateLabel, StateCurrent, StateDelinquent, + ), + StateLabel, + ), AccountBalances: NewGaugeDesc( "solana_account_balance", fmt.Sprintf("Solana account balances, grouped by %s", AddressLabel), @@ -103,9 +132,13 @@ func NewSolanaCollector(client *rpc.Client, config *ExporterConfig) *SolanaColle func (c *SolanaCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.NodeVersion.Desc ch <- c.ValidatorActiveStake.Desc + ch <- c.ClusterActiveStake.Desc ch <- c.ValidatorLastVote.Desc + ch <- c.ClusterLastVote.Desc ch <- c.ValidatorRootSlot.Desc + ch <- c.ClusterRootSlot.Desc ch <- c.ValidatorDelinquent.Desc + ch <- c.ClusterValidatorCount.Desc ch <- c.AccountBalances.Desc ch <- c.NodeIsHealthy.Desc ch <- c.NodeNumSlotsBehind.Desc @@ -123,26 +156,58 @@ func (c *SolanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro if err != nil { c.logger.Errorf("failed to get vote accounts: %v", err) ch <- c.ValidatorActiveStake.NewInvalidMetric(err) + ch <- c.ClusterActiveStake.NewInvalidMetric(err) ch <- c.ValidatorLastVote.NewInvalidMetric(err) + ch <- c.ClusterLastVote.NewInvalidMetric(err) ch <- c.ValidatorRootSlot.NewInvalidMetric(err) + ch <- c.ClusterRootSlot.NewInvalidMetric(err) ch <- c.ValidatorDelinquent.NewInvalidMetric(err) + ch <- c.ClusterValidatorCount.NewInvalidMetric(err) return } + var ( + totalStake float64 + maxLastVote float64 + maxRootSlot float64 + ) for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) { accounts := []string{account.VotePubkey, account.NodePubkey} - ch <- c.ValidatorActiveStake.MustNewConstMetric(float64(account.ActivatedStake)/rpc.LamportsInSol, accounts...) - ch <- c.ValidatorLastVote.MustNewConstMetric(float64(account.LastVote), accounts...) - ch <- c.ValidatorRootSlot.MustNewConstMetric(float64(account.RootSlot), accounts...) - } + stake, lastVote, rootSlot := + float64(account.ActivatedStake)/rpc.LamportsInSol, + float64(account.LastVote), + float64(account.RootSlot) - for _, account := range voteAccounts.Current { - ch <- c.ValidatorDelinquent.MustNewConstMetric(0, account.VotePubkey, account.NodePubkey) + if slices.Contains(c.config.NodeKeys, account.NodePubkey) || c.config.ComprehensiveVoteAccountTracking { + ch <- c.ValidatorActiveStake.MustNewConstMetric(stake, accounts...) + ch <- c.ValidatorLastVote.MustNewConstMetric(lastVote, accounts...) + ch <- c.ValidatorRootSlot.MustNewConstMetric(rootSlot, accounts...) + } + + totalStake += stake + maxLastVote = max(maxLastVote, lastVote) + maxRootSlot = max(maxRootSlot, rootSlot) } - for _, account := range voteAccounts.Delinquent { - ch <- c.ValidatorDelinquent.MustNewConstMetric(1, account.VotePubkey, account.NodePubkey) + + { + for _, account := range voteAccounts.Current { + if slices.Contains(c.config.NodeKeys, account.NodePubkey) || c.config.ComprehensiveVoteAccountTracking { + ch <- c.ValidatorDelinquent.MustNewConstMetric(0, account.VotePubkey, account.NodePubkey) + } + } + for _, account := range voteAccounts.Delinquent { + if slices.Contains(c.config.NodeKeys, account.NodePubkey) || c.config.ComprehensiveVoteAccountTracking { + ch <- c.ValidatorDelinquent.MustNewConstMetric(1, account.VotePubkey, account.NodePubkey) + } + } } + ch <- c.ClusterActiveStake.MustNewConstMetric(totalStake) + ch <- c.ClusterLastVote.MustNewConstMetric(maxLastVote) + ch <- c.ClusterRootSlot.MustNewConstMetric(maxRootSlot) + ch <- c.ClusterValidatorCount.MustNewConstMetric(float64(len(voteAccounts.Current)), StateCurrent) + ch <- c.ClusterValidatorCount.MustNewConstMetric(float64(len(voteAccounts.Delinquent)), StateDelinquent) + c.logger.Info("Vote accounts collected.") } diff --git a/cmd/solana-exporter/collector_test.go b/cmd/solana-exporter/collector_test.go index d7e2f08..55adbb8 100644 --- a/cmd/solana-exporter/collector_test.go +++ b/cmd/solana-exporter/collector_test.go @@ -33,6 +33,8 @@ type ( Votekeys []string FeeRewardLamports int InflationRewardLamports int + LastVoteDistances map[string]int + RootSlotDistances map[string]int } ) @@ -86,6 +88,8 @@ func NewSimulator(t *testing.T, slot int) (*Simulator, *rpc.Client) { Votekeys: votekeys, InflationRewardLamports: inflationRewardLamports, FeeRewardLamports: feeRewardLamports, + LastVoteDistances: map[string]int{"aaa": 1, "bbb": 2, "ccc": 3}, + RootSlotDistances: map[string]int{"aaa": 4, "bbb": 5, "ccc": 6}, } simulator.PopulateSlot(0) if slot > 0 { @@ -144,7 +148,8 @@ func (c *Simulator) PopulateSlot(slot int) { for _, nodekey := range c.Nodekeys { transactions = append(transactions, []string{nodekey, strings.ToUpper(nodekey), VoteProgram}) info := c.Server.GetValidatorInfo(nodekey) - info.LastVote = slot + info.LastVote = max(0, slot-c.LastVoteDistances[nodekey]) + info.RootSlot = max(0, slot-c.RootSlotDistances[nodekey]) c.Server.SetOpt(rpc.ValidatorInfoOpt, nodekey, info) } @@ -199,6 +204,7 @@ func newTestConfig(simulator *Simulator, fast bool) *ExporterConfig { nil, true, true, + true, false, pace, } @@ -218,21 +224,34 @@ func TestSolanaCollector(t *testing.T) { NewLV(stake, "bbb", "BBB"), NewLV(stake, "ccc", "CCC"), ), + collector.ClusterActiveStake.makeCollectionTest( + NewLV(3 * stake), + ), collector.ValidatorLastVote.makeCollectionTest( - NewLV(34, "aaa", "AAA"), - NewLV(34, "bbb", "BBB"), - NewLV(34, "ccc", "CCC"), + NewLV(33, "aaa", "AAA"), + NewLV(32, "bbb", "BBB"), + NewLV(31, "ccc", "CCC"), + ), + collector.ClusterLastVote.makeCollectionTest( + NewLV(33), ), collector.ValidatorRootSlot.makeCollectionTest( - NewLV(0, "aaa", "AAA"), - NewLV(0, "bbb", "BBB"), - NewLV(0, "ccc", "CCC"), + NewLV(30, "aaa", "AAA"), + NewLV(29, "bbb", "BBB"), + NewLV(28, "ccc", "CCC"), + ), + collector.ClusterRootSlot.makeCollectionTest( + NewLV(30), ), collector.ValidatorDelinquent.makeCollectionTest( NewLV(0, "aaa", "AAA"), NewLV(0, "bbb", "BBB"), NewLV(0, "ccc", "CCC"), ), + collector.ClusterValidatorCount.makeCollectionTest( + NewLV(3, StateCurrent), + NewLV(0, StateDelinquent), + ), collector.NodeVersion.makeCollectionTest( NewLV(1, "v1.0.0"), ), @@ -258,6 +277,7 @@ func TestSolanaCollector(t *testing.T) { ), } + fmt.Println(testCases[1].ExpectedResponse) for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { err := testutil.CollectAndCompare(collector, bytes.NewBufferString(test.ExpectedResponse), test.Name) diff --git a/cmd/solana-exporter/config.go b/cmd/solana-exporter/config.go index ba9128b..39ac8b7 100644 --- a/cmd/solana-exporter/config.go +++ b/cmd/solana-exporter/config.go @@ -13,16 +13,17 @@ type ( arrayFlags []string ExporterConfig struct { - HttpTimeout time.Duration - RpcUrl string - ListenAddress string - NodeKeys []string - VoteKeys []string - BalanceAddresses []string - ComprehensiveSlotTracking bool - MonitorBlockSizes bool - LightMode bool - SlotPace time.Duration + HttpTimeout time.Duration + RpcUrl string + ListenAddress string + NodeKeys []string + VoteKeys []string + BalanceAddresses []string + ComprehensiveSlotTracking bool + ComprehensiveVoteAccountTracking bool + MonitorBlockSizes bool + LightMode bool + SlotPace time.Duration } ) @@ -43,6 +44,7 @@ func NewExporterConfig( nodeKeys []string, balanceAddresses []string, comprehensiveSlotTracking bool, + comprehensiveVoteAccountTracking bool, monitorBlockSizes bool, lightMode bool, slotPace time.Duration, @@ -56,6 +58,7 @@ func NewExporterConfig( "nodeKeys", nodeKeys, "balanceAddresses", balanceAddresses, "comprehensiveSlotTracking", comprehensiveSlotTracking, + "comprehensiveVoteAccountTracking", comprehensiveVoteAccountTracking, "monitorBlockSizes", monitorBlockSizes, "lightMode", lightMode, ) @@ -64,6 +67,10 @@ func NewExporterConfig( return nil, fmt.Errorf("'-light-mode' is incompatible with `-comprehensive-slot-tracking`") } + if comprehensiveVoteAccountTracking { + return nil, fmt.Errorf("'-light-mode' is incompatible with '-comprehensive-vote-account-tracking'") + } + if monitorBlockSizes { return nil, fmt.Errorf("'-light-mode' is incompatible with `-monitor-block-sizes`") } @@ -87,31 +94,33 @@ func NewExporterConfig( } config := ExporterConfig{ - HttpTimeout: httpTimeout, - RpcUrl: rpcUrl, - ListenAddress: listenAddress, - NodeKeys: nodeKeys, - VoteKeys: voteKeys, - BalanceAddresses: balanceAddresses, - ComprehensiveSlotTracking: comprehensiveSlotTracking, - MonitorBlockSizes: monitorBlockSizes, - LightMode: lightMode, - SlotPace: slotPace, + HttpTimeout: httpTimeout, + RpcUrl: rpcUrl, + ListenAddress: listenAddress, + NodeKeys: nodeKeys, + VoteKeys: voteKeys, + BalanceAddresses: balanceAddresses, + ComprehensiveSlotTracking: comprehensiveSlotTracking, + ComprehensiveVoteAccountTracking: comprehensiveVoteAccountTracking, + MonitorBlockSizes: monitorBlockSizes, + LightMode: lightMode, + SlotPace: slotPace, } return &config, nil } func NewExporterConfigFromCLI(ctx context.Context) (*ExporterConfig, error) { var ( - httpTimeout int - rpcUrl string - listenAddress string - nodekeys arrayFlags - balanceAddresses arrayFlags - comprehensiveSlotTracking bool - monitorBlockSizes bool - lightMode bool - slotPace int + httpTimeout int + rpcUrl string + listenAddress string + nodekeys arrayFlags + balanceAddresses arrayFlags + comprehensiveSlotTracking bool + comprehensiveVoteAccountTracking bool + monitorBlockSizes bool + lightMode bool + slotPace int ) flag.IntVar( &httpTimeout, @@ -147,9 +156,16 @@ func NewExporterConfigFromCLI(ctx context.Context) (*ExporterConfig, error) { &comprehensiveSlotTracking, "comprehensive-slot-tracking", false, - "Set this flag to track solana_leader_slots_by_epoch for ALL validators. "+ + "Set this flag to track solana_validator_leader_slots_by_epoch for all validators. "+ "Warning: this will lead to potentially thousands of new Prometheus metrics being created every epoch.", ) + flag.BoolVar( + &comprehensiveVoteAccountTracking, + "comprehensive-vote-account-tracking", + false, + "Set this flag to track vote-account metrics such as solana_validator_active_stake for all validators. "+ + "Warning: this will lead to potentially thousands of Prometheus metrics.", + ) flag.BoolVar( &monitorBlockSizes, "monitor-block-sizes", @@ -181,6 +197,7 @@ func NewExporterConfigFromCLI(ctx context.Context) (*ExporterConfig, error) { nodekeys, balanceAddresses, comprehensiveSlotTracking, + comprehensiveVoteAccountTracking, monitorBlockSizes, lightMode, time.Duration(slotPace)*time.Second, diff --git a/cmd/solana-exporter/config_test.go b/cmd/solana-exporter/config_test.go index 520033a..c19f27e 100644 --- a/cmd/solana-exporter/config_test.go +++ b/cmd/solana-exporter/config_test.go @@ -10,60 +10,64 @@ import ( func TestNewExporterConfig(t *testing.T) { simulator, _ := NewSimulator(t, 35) tests := []struct { - name string - httpTimeout time.Duration - rpcUrl string - listenAddress string - nodeKeys []string - balanceAddresses []string - comprehensiveSlotTracking bool - monitorBlockSizes bool - lightMode bool - slotPace time.Duration - wantErr bool - expectedVoteKeys []string + name string + httpTimeout time.Duration + rpcUrl string + listenAddress string + nodeKeys []string + balanceAddresses []string + comprehensiveSlotTracking bool + comprehensiveVoteAccountTracking bool + monitorBlockSizes bool + lightMode bool + slotPace time.Duration + wantErr bool + expectedVoteKeys []string }{ { - name: "valid configuration", - httpTimeout: 60 * time.Second, - rpcUrl: simulator.Server.URL(), - listenAddress: ":8080", - nodeKeys: simulator.Nodekeys, - balanceAddresses: []string{"xxx", "yyy", "zzz"}, - comprehensiveSlotTracking: false, - monitorBlockSizes: false, - lightMode: false, - slotPace: time.Second, - wantErr: false, - expectedVoteKeys: simulator.Votekeys, + name: "valid configuration", + httpTimeout: 60 * time.Second, + rpcUrl: simulator.Server.URL(), + listenAddress: ":8080", + nodeKeys: simulator.Nodekeys, + balanceAddresses: []string{"xxx", "yyy", "zzz"}, + comprehensiveSlotTracking: false, + comprehensiveVoteAccountTracking: false, + monitorBlockSizes: false, + lightMode: false, + slotPace: time.Second, + wantErr: false, + expectedVoteKeys: simulator.Votekeys, }, { - name: "light mode with incompatible options", - httpTimeout: 60 * time.Second, - rpcUrl: simulator.Server.URL(), - listenAddress: ":8080", - nodeKeys: simulator.Nodekeys, - balanceAddresses: []string{"xxx", "yyy", "zzz"}, - comprehensiveSlotTracking: false, - monitorBlockSizes: false, - lightMode: true, - slotPace: time.Second, - wantErr: true, - expectedVoteKeys: nil, + name: "light mode with incompatible options", + httpTimeout: 60 * time.Second, + rpcUrl: simulator.Server.URL(), + listenAddress: ":8080", + nodeKeys: simulator.Nodekeys, + balanceAddresses: []string{"xxx", "yyy", "zzz"}, + comprehensiveSlotTracking: false, + comprehensiveVoteAccountTracking: false, + monitorBlockSizes: false, + lightMode: true, + slotPace: time.Second, + wantErr: true, + expectedVoteKeys: nil, }, { - name: "empty node keys", - httpTimeout: 60 * time.Second, - rpcUrl: simulator.Server.URL(), - listenAddress: ":8080", - nodeKeys: []string{}, - balanceAddresses: []string{"xxx", "yyy", "zzz"}, - comprehensiveSlotTracking: false, - monitorBlockSizes: false, - lightMode: false, - slotPace: time.Second, - wantErr: false, - expectedVoteKeys: []string{}, + name: "empty node keys", + httpTimeout: 60 * time.Second, + rpcUrl: simulator.Server.URL(), + listenAddress: ":8080", + nodeKeys: []string{}, + balanceAddresses: []string{"xxx", "yyy", "zzz"}, + comprehensiveSlotTracking: false, + comprehensiveVoteAccountTracking: false, + monitorBlockSizes: false, + lightMode: false, + slotPace: time.Second, + wantErr: false, + expectedVoteKeys: []string{}, }, } @@ -77,6 +81,7 @@ func TestNewExporterConfig(t *testing.T) { tt.nodeKeys, tt.balanceAddresses, tt.comprehensiveSlotTracking, + tt.comprehensiveVoteAccountTracking, tt.monitorBlockSizes, tt.lightMode, tt.slotPace, diff --git a/pkg/rpc/mock.go b/pkg/rpc/mock.go index 0e1b209..34f6dfc 100644 --- a/pkg/rpc/mock.go +++ b/pkg/rpc/mock.go @@ -54,6 +54,7 @@ type ( Stake int LastVote int Delinquent bool + RootSlot int } ) @@ -244,7 +245,7 @@ func (s *MockServer) getResult(method string, params ...any) (any, *RPCError) { "activatedStake": int64(info.Stake), "lastVote": info.LastVote, "nodePubkey": nodekey, - "rootSlot": 0, + "rootSlot": info.RootSlot, "votePubkey": info.Votekey, } if info.Delinquent { diff --git a/pkg/rpc/mock_test.go b/pkg/rpc/mock_test.go index cebd31f..c883fb3 100644 --- a/pkg/rpc/mock_test.go +++ b/pkg/rpc/mock_test.go @@ -121,9 +121,9 @@ func TestMockServer_getVoteAccounts(t *testing.T) { nil, nil, map[string]MockValidatorInfo{ - "aaa": {"AAA", 1, 2, false}, - "bbb": {"BBB", 3, 4, false}, - "ccc": {"CCC", 5, 6, true}, + "aaa": {"AAA", 1, 2, false, 10}, + "bbb": {"BBB", 3, 4, false, 11}, + "ccc": {"CCC", 5, 6, true, 12}, }, ) ctx, cancel := context.WithCancel(context.Background()) @@ -138,11 +138,11 @@ func TestMockServer_getVoteAccounts(t *testing.T) { assert.Equal(t, VoteAccounts{ Current: []VoteAccount{ - {1, 2, "aaa", 0, "AAA"}, - {3, 4, "bbb", 0, "BBB"}, + {1, 2, "aaa", 10, "AAA"}, + {3, 4, "bbb", 11, "BBB"}, }, Delinquent: []VoteAccount{ - {5, 6, "ccc", 0, "CCC"}, + {5, 6, "ccc", 12, "CCC"}, }, }, *voteAccounts,