diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d36d33b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,99 @@ +# v3.0.0 + +## Key Changes + +The new `solana-exporter` (renamed from `solana_exporter`) contains many new metrics, standardised naming conventions +and more configurability. + +## What's Changed +### Metric Updates +#### New Metrics + +Below is a list of newly added metrics (see the [README](README.md) +for metric descriptions): + +* `solana_account_balance` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_node_is_healthy` ([@GranderStark](https://github.com/GranderStark)) +* `solana_nude_num_slots_behind` ([@GranderStark](https://github.com/GranderStark)) +* `solana_node_minimum_ledger_slot` ([@GranderStark](https://github.com/GranderStark)) +* `solana_node_first_available_block` ([@GranderStark](https://github.com/GranderStark)) +* `solana_cluster_slots_by_epoch_total` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_validator_fee_rewards` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_validator_block_size` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_node_block_height` ([@GranderStark](https://github.com/GranderStark)) +* `solana_cluster_active_stake` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_cluster_last_vote` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_cluster_root_slot` ([@johnstonematt](https://github.com/johnstonematt)) +* `solana_cluster_validator_count` ([@johnstonematt](https://github.com/johnstonematt)) + +#### Renamed Metrics + +The table below contains all metrics renamed in `v3.0.0` ([@johnstonematt](https://github.com/johnstonematt)): + +| Old Name | New Name | +|---------------------------------------|------------------------------------------------| +| `solana_validator_activated_stake` | `solana_validator_active_stake` | +| `solana_confirmed_transactions_total` | `solana_node_transactions_total` | +| `solana_confirmed_slot_height` | `solana_node_slot_height` | +| `solana_confirmed_epoch_number` | `solana_node_epoch_number` | +| `solana_confirmed_epoch_first_slot` | `solana_node_epoch_first_slot` | +| `solana_confirmed_epoch_last_slot` | `solana_node_epoch_last_slot` | +| `solana_leader_slots_total` | `solana_validator_leader_slots_total` | +| `solana_leader_slots_by_epoch` | `solana_validator_leader_slots_by_epoch_total` | +| `solana_active_validators` | `solana_cluster_validator_count` | + +Metrics were renamed to: +* Remove commitment levels from metric names. +* Standardise naming conventions: + * `solana_validator_*`: Validator-specific metrics which are trackable from any RPC node (i.e., active stake). + * `solana_node_*`: Node-specific metrics which are not trackable from other nodes (i.e., node health). + +#### Label Updates + +The following labels were renamed ([@johnstonematt](https://github.com/johnstonematt)): + * `pubkey` was renamed to `votekey`, to clearly identity that it refers to the address of a validators vote account. + +### Config Updates +#### New Config Parameters + +Below is a list of newly added config parameters (see the [README](README.md) +for parameter descriptions) ([@johnstonematt](https://github.com/johnstonematt)): + + * `-balance-address` + * `-nodekey` + * `-comprehensive-slot-tracking` + * `-monitor-block-sizes` + * `-slot-pace` + * `-light-mode` + * `-http-timeout` + * `-comprehensive-vote-account-tracking` + +#### Renamed Config Parameters + +The table below contains all config parameters renamed in `v3.0.0` ([@johnstonematt](https://github.com/johnstonematt)): + +| Old Name | New Name | +|-------------------------------------|-------------------| +| `-rpcURI` | `-rpc-url` | +| `addr` | `-listen-address` | + +#### Removed Config Parameters + +The following metrics were removed ([@johnstonematt](https://github.com/johnstonematt)): + + * `votepubkey`. Configure validator tracking using the `-nodekey` parameter. + +### General Updates + +* The project was renamed from `solana_exporter` to `solana-exporter`, to conform with +[Go naming conventions](https://github.com/unknwon/go-code-convention/blob/main/en-US.md) ([@johnstonematt](https://github.com/johnstonematt)). +* Testing was significantly improved ([@johnstonematt](https://github.com/johnstonematt)). +* [klog](https://github.com/kubernetes/klog) logging was removed and replaced with [zap](https://github.com/uber-go/zap) + ([@johnstonematt](https://github.com/johnstonematt)) +* Easy usage ([@johnstonematt](https://github.com/johnstonematt)): + * The example dashboard was updated. + * An example prometheus config was added, as well as recording rules for tracking skip rate. + +## New Contributors + +* [@GranderStark](https://github.com/GranderStark) made their first contribution. diff --git a/README.md b/README.md index 3e9d3ca..24a7e7d 100644 --- a/README.md +++ b/README.md @@ -36,18 +36,19 @@ CGO_ENABLED=0 go build ./cmd/solana-exporter The exporter is configured via the following command line arguments: -| Option | Description | Default | -|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| 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` | | `-active-identity` | Validator identity public key used to determine if the node is considered active in the `solana_node_is_active` metric. | N/A | -| `-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` | ### Notes on Configuration @@ -62,48 +63,68 @@ 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 | +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 | | `solana_node_is_active` | Whether the node is active and participating in consensus. | `identity` | -| `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 | | `solana_foundation_min_required_version` | Minimum required Solana version for the [solana foundation delegation program](https://solana.org/delegation-program) | `version`, `cluster` | -***NOTE***: An `*` in the description indicates that the metric **is** tracked in `-light-mode`. +#### 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` | +| 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` | | `cluster` | Solana cluster name. | `mainnet-beta`, `testnet`, `devnet` | -| `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 da46a17..499aa39 100644 --- a/cmd/solana-exporter/collector.go +++ b/cmd/solana-exporter/collector.go @@ -10,10 +10,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" @@ -26,6 +28,9 @@ const ( StatusSkipped = "skipped" StatusValid = "valid" + StateCurrent = "current" + StateDelinquent = "delinquent" + TransactionTypeVote = "vote" TransactionTypeNonVote = "non_vote" ) @@ -39,9 +44,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 NodeIdentity *GaugeDesc @@ -64,21 +73,41 @@ func NewSolanaCollector(rpcClient *rpc.Client, apiClient *api.Client, config *Ex 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), @@ -128,9 +157,13 @@ func (c *SolanaCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.NodeVersion.Desc ch <- c.NodeIdentity.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 @@ -150,26 +183,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) + + 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...) + } - for _, account := range voteAccounts.Current { - ch <- c.ValidatorDelinquent.MustNewConstMetric(0, account.VotePubkey, account.NodePubkey) + 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 5705caa..f8cac33 100644 --- a/cmd/solana-exporter/collector_test.go +++ b/cmd/solana-exporter/collector_test.go @@ -35,6 +35,8 @@ type ( Votekeys []string FeeRewardLamports int InflationRewardLamports int + LastVoteDistances map[string]int + RootSlotDistances map[string]int } ) @@ -89,6 +91,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 { @@ -147,7 +151,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) } @@ -231,21 +236,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"), ), @@ -280,6 +298,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 ce98019..e096794 100644 --- a/cmd/solana-exporter/config.go +++ b/cmd/solana-exporter/config.go @@ -14,17 +14,18 @@ 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 - ActiveIdentity string + HttpTimeout time.Duration + RpcUrl string + ListenAddress string + NodeKeys []string + VoteKeys []string + BalanceAddresses []string + ComprehensiveSlotTracking bool + ComprehensiveVoteAccountTracking bool + MonitorBlockSizes bool + LightMode bool + SlotPace time.Duration + ActiveIdentity string } ) @@ -45,6 +46,7 @@ func NewExporterConfig( nodeKeys []string, balanceAddresses []string, comprehensiveSlotTracking bool, + comprehensiveVoteAccountTracking bool, monitorBlockSizes bool, lightMode bool, slotPace time.Duration, @@ -59,6 +61,7 @@ func NewExporterConfig( "nodeKeys", nodeKeys, "balanceAddresses", balanceAddresses, "comprehensiveSlotTracking", comprehensiveSlotTracking, + "comprehensiveVoteAccountTracking", comprehensiveVoteAccountTracking, "monitorBlockSizes", monitorBlockSizes, "lightMode", lightMode, "activeIdentity", activeIdentity, @@ -68,6 +71,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`") } @@ -91,33 +98,35 @@ func NewExporterConfig( } config := ExporterConfig{ - HttpTimeout: httpTimeout, - RpcUrl: rpcUrl, - ListenAddress: listenAddress, - NodeKeys: nodeKeys, - VoteKeys: voteKeys, - BalanceAddresses: balanceAddresses, - ComprehensiveSlotTracking: comprehensiveSlotTracking, - MonitorBlockSizes: monitorBlockSizes, - LightMode: lightMode, - SlotPace: slotPace, - ActiveIdentity: activeIdentity, + HttpTimeout: httpTimeout, + RpcUrl: rpcUrl, + ListenAddress: listenAddress, + NodeKeys: nodeKeys, + VoteKeys: voteKeys, + BalanceAddresses: balanceAddresses, + ComprehensiveSlotTracking: comprehensiveSlotTracking, + ComprehensiveVoteAccountTracking: comprehensiveVoteAccountTracking, + MonitorBlockSizes: monitorBlockSizes, + LightMode: lightMode, + SlotPace: slotPace, + ActiveIdentity: activeIdentity, } 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 - activeIdentity string + httpTimeout int + rpcUrl string + listenAddress string + nodekeys arrayFlags + balanceAddresses arrayFlags + comprehensiveSlotTracking bool + comprehensiveVoteAccountTracking bool + monitorBlockSizes bool + lightMode bool + slotPace int + activeIdentity string ) flag.IntVar( &httpTimeout, @@ -153,9 +162,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", @@ -193,6 +209,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 42a22d8..804754e 100644 --- a/cmd/solana-exporter/config_test.go +++ b/cmd/solana-exporter/config_test.go @@ -11,64 +11,68 @@ 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 - activeIdentity 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 + activeIdentity 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, - activeIdentity: simulator.Nodekeys[0], + 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, + activeIdentity: simulator.Nodekeys[0], }, { - 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, - activeIdentity: simulator.Nodekeys[0], + 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, + activeIdentity: simulator.Nodekeys[0], }, { - 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{}, - activeIdentity: simulator.Nodekeys[0], + 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{}, + activeIdentity: simulator.Nodekeys[0], }, } @@ -82,6 +86,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 486c88d..4da35df 100644 --- a/pkg/rpc/mock.go +++ b/pkg/rpc/mock.go @@ -55,6 +55,7 @@ type ( Stake int LastVote int Delinquent bool + RootSlot int } ) @@ -245,7 +246,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,