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,