From 6f4fcad90d2f00cd1e7537809940b848174cf8ef Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Mon, 14 Oct 2024 14:39:41 +0200 Subject: [PATCH] refactored config --- cmd/solana_exporter/exporter.go | 192 +++++++++++---------------- cmd/solana_exporter/exporter_test.go | 108 +++++++-------- cmd/solana_exporter/slots.go | 50 ++++--- cmd/solana_exporter/slots_test.go | 11 +- cmd/solana_exporter/utils.go | 53 ++++++++ cmd/solana_exporter/utils_test.go | 23 ++++ 6 files changed, 234 insertions(+), 203 deletions(-) diff --git a/cmd/solana_exporter/exporter.go b/cmd/solana_exporter/exporter.go index 1d2325d..81e759e 100644 --- a/cmd/solana_exporter/exporter.go +++ b/cmd/solana_exporter/exporter.go @@ -13,33 +13,45 @@ import ( "k8s.io/klog/v2" ) +const ( + SkipStatusLabel = "status" + StateLabel = "state" + NodekeyLabel = "nodekey" + VotekeyLabel = "votekey" + VersionLabel = "version" + AddressLabel = "address" + EpochLabel = "epoch" + + StatusSkipped = "skipped" + StatusValid = "valid" + + StateCurrent = "current" + StateDelinquent = "delinquent" +) + var ( httpTimeout = 60 * time.Second - rpcAddr = flag.String("rpcURI", "", "Solana RPC URI (including protocol and path)") - addr = flag.String("addr", ":8080", "Listen address") - votePubkey = flag.String("votepubkey", "", "Validator vote address (will only return results of this address)") - httpTimeoutSecs = flag.Int("http_timeout", 60, "HTTP timeout in seconds") + rpcUrl = flag.String("rpc-url", "", "Solana RPC URI (including protocol and path)") + listenAddress = flag.String("listen-address", ":8080", "Listen address") + httpTimeoutSecs = flag.Int("http-timeout", 60, "HTTP timeout to use, in seconds.") // addresses: + nodekeys = flag.String( + "nodekeys", + "", + "Comma-separated list of nodekeys (identity accounts) representing validators to monitor.", + ) + comprehensiveSlotTracking = flag.Bool( + "comprehensive-slot-tracking", + false, + "Set this flag to track solana_leader_slots_by_epoch for ALL validators. "+ + "Warning: this will lead to potentially thousands of new Prometheus metrics being created every epoch.", + ) balanceAddresses = flag.String( "balance-addresses", "", - "Comma-separated list of addresses to monitor SOL balances.", - ) - leaderSlotAddresses = flag.String( - "leader-slot-addresses", - "", - "Comma-separated list of addresses to monitor leader slots by epoch for, leave nil to track by epoch for all validators (this creates a lot of Prometheus metrics with every new epoch).", - ) - inflationRewardAddresses = flag.String( - "inflation-reward-addresses", - "", - "Comma-separated list of validator vote accounts to track inflationary rewards for", - ) - feeRewardAddresses = flag.String( - "fee-reward-addresses", - "", - "Comma-separated list of validator identity accounts to track fee rewards for.", + "Comma-separated list of addresses to monitor SOL balances for, "+ + "in addition to the identity and vote accounts of the provided nodekeys.", ) ) @@ -47,15 +59,12 @@ func init() { klog.InitFlags(nil) } -type solanaCollector struct { +type SolanaCollector struct { rpcClient rpc.Provider // config: - slotPace time.Duration - balanceAddresses []string - leaderSlotAddresses []string - inflationRewardAddresses []string - feeRewardAddresses []string + slotPace time.Duration + balanceAddresses []string /// descriptors: totalValidatorsDesc *prometheus.Desc @@ -67,84 +76,60 @@ type solanaCollector struct { balances *prometheus.Desc } -func createSolanaCollector( - provider rpc.Provider, - slotPace time.Duration, - balanceAddresses []string, - leaderSlotAddresses []string, - inflationRewardAddresses []string, - feeRewardAddresses []string, -) *solanaCollector { - return &solanaCollector{ - rpcClient: provider, - slotPace: slotPace, - balanceAddresses: balanceAddresses, - leaderSlotAddresses: leaderSlotAddresses, - inflationRewardAddresses: inflationRewardAddresses, - feeRewardAddresses: feeRewardAddresses, +func NewSolanaCollector( + provider rpc.Provider, slotPace time.Duration, balanceAddresses []string, nodekeys []string, votekeys []string, +) *SolanaCollector { + collector := &SolanaCollector{ + rpcClient: provider, + slotPace: slotPace, + balanceAddresses: CombineUnique(balanceAddresses, nodekeys, votekeys), totalValidatorsDesc: prometheus.NewDesc( "solana_active_validators", "Total number of active validators by state", - []string{"state"}, + []string{StateLabel}, nil, ), validatorActivatedStake: prometheus.NewDesc( "solana_validator_activated_stake", "Activated stake per validator", - []string{"pubkey", "nodekey"}, + []string{VotekeyLabel, NodekeyLabel}, nil, ), validatorLastVote: prometheus.NewDesc( "solana_validator_last_vote", "Last voted slot per validator", - []string{"pubkey", "nodekey"}, + []string{VotekeyLabel, NodekeyLabel}, nil, ), validatorRootSlot: prometheus.NewDesc( "solana_validator_root_slot", "Root slot per validator", - []string{"pubkey", "nodekey"}, + []string{VotekeyLabel, NodekeyLabel}, nil, ), validatorDelinquent: prometheus.NewDesc( "solana_validator_delinquent", "Whether a validator is delinquent", - []string{"pubkey", "nodekey"}, + []string{VotekeyLabel, NodekeyLabel}, nil, ), solanaVersion: prometheus.NewDesc( "solana_node_version", "Node version of solana", - []string{"version"}, + []string{VersionLabel}, nil, ), balances: prometheus.NewDesc( "solana_account_balance", "Solana account balances", - []string{"address"}, + []string{AddressLabel}, nil, ), } + return collector } -func NewSolanaCollector( - rpcAddr string, - balanceAddresses []string, - leaderSlotAddresses []string, - inflationRewardAddresses []string, - feeRewardAddresses []string, -) *solanaCollector { - return createSolanaCollector( - rpc.NewRPCClient(rpcAddr), - slotPacerSchedule, - balanceAddresses, - leaderSlotAddresses, - inflationRewardAddresses, - feeRewardAddresses, - ) -} - -func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) { +func (c *SolanaCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.totalValidatorsDesc ch <- c.solanaVersion ch <- c.validatorActivatedStake @@ -154,8 +139,8 @@ func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.balances } -func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) { - voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, votePubkey) +func (c *SolanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) { + voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, nil) if err != nil { ch <- prometheus.NewInvalidMetric(c.totalValidatorsDesc, err) ch <- prometheus.NewInvalidMetric(c.validatorActivatedStake, err) @@ -166,10 +151,10 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro } ch <- prometheus.MustNewConstMetric( - c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), "delinquent", + c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), StateDelinquent, ) ch <- prometheus.MustNewConstMetric( - c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Current)), "current", + c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Current)), StateCurrent, ) for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) { @@ -208,7 +193,7 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro } } -func (c *solanaCollector) collectVersion(ctx context.Context, ch chan<- prometheus.Metric) { +func (c *SolanaCollector) collectVersion(ctx context.Context, ch chan<- prometheus.Metric) { version, err := c.rpcClient.GetVersion(ctx) if err != nil { @@ -219,8 +204,8 @@ func (c *solanaCollector) collectVersion(ctx context.Context, ch chan<- promethe ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version) } -func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) { - balances, err := fetchBalances(ctx, c.rpcClient, c.balanceAddresses) +func (c *SolanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) { + balances, err := FetchBalances(ctx, c.rpcClient, c.balanceAddresses) if err != nil { ch <- prometheus.NewInvalidMetric(c.solanaVersion, err) return @@ -231,19 +216,7 @@ func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometh } } -func fetchBalances(ctx context.Context, client rpc.Provider, addresses []string) (map[string]float64, error) { - balances := make(map[string]float64) - for _, address := range addresses { - balance, err := client.GetBalance(ctx, rpc.CommitmentConfirmed, address) - if err != nil { - return nil, err - } - balances[address] = balance - } - return balances, nil -} - -func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) { +func (c *SolanaCollector) Collect(ch chan<- prometheus.Metric) { ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) defer cancel() @@ -253,15 +226,16 @@ func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) { } func main() { + ctx := context.Background() flag.Parse() - if *rpcAddr == "" { + if *rpcUrl == "" { klog.Fatal("Please specify -rpcURI") } - if *leaderSlotAddresses == "" { + if *comprehensiveSlotTracking { klog.Warning( - "Not specifying leader-slot-addresses will lead to potentially thousands of new " + + "Comprehensive slot tracking will lead to potentially thousands of new " + "Prometheus metrics being created every epoch.", ) } @@ -269,37 +243,31 @@ func main() { httpTimeout = time.Duration(*httpTimeoutSecs) * time.Second var ( - balAddresses []string - lsAddresses []string - irAddresses []string - frAddresses []string + balAddresses []string + validatorNodekeys []string ) if *balanceAddresses != "" { balAddresses = strings.Split(*balanceAddresses, ",") - klog.Infof("Monitoring balances for %v", balAddresses) } - if *leaderSlotAddresses != "" { - lsAddresses = strings.Split(*leaderSlotAddresses, ",") - klog.Infof("Monitoring leader-slot by epoch for %v", lsAddresses) - - } - if *inflationRewardAddresses != "" { - irAddresses = strings.Split(*inflationRewardAddresses, ",") - klog.Infof("Monitoring inflation reward by epoch for %v", irAddresses) - } - if *feeRewardAddresses != "" { - frAddresses = strings.Split(*feeRewardAddresses, ",") - klog.Infof("Monitoring fee reward by epoch for %v", frAddresses) + if *nodekeys != "" { + validatorNodekeys = strings.Split(*nodekeys, ",") + klog.Infof("Monitoring the following validators: %v", validatorNodekeys) } - collector := NewSolanaCollector(*rpcAddr, balAddresses, lsAddresses, irAddresses, frAddresses) - - slotWatcher := NewCollectorSlotWatcher(collector) - go slotWatcher.WatchSlots(context.Background(), collector.slotPace) + client := rpc.NewRPCClient(*rpcUrl) + ctx_, cancel := context.WithTimeout(ctx, httpTimeout) + defer cancel() + votekeys, err := GetAssociatedVoteAccounts(ctx_, client, rpc.CommitmentFinalized, validatorNodekeys) + if err != nil { + klog.Fatalf("Failed to get associated vote accounts for %v: %v", nodekeys, err) + } + collector := NewSolanaCollector(client, slotPacerSchedule, balAddresses, validatorNodekeys, votekeys) + slotWatcher := NewSlotWatcher(client, validatorNodekeys, votekeys, *comprehensiveSlotTracking) + go slotWatcher.WatchSlots(ctx, collector.slotPace) prometheus.MustRegister(collector) http.Handle("/metrics", promhttp.Handler()) - klog.Infof("listening on %s", *addr) - klog.Fatal(http.ListenAndServe(*addr, nil)) + klog.Infof("listening on %s", *listenAddress) + klog.Fatal(http.ListenAndServe(*listenAddress, nil)) } diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go index 455869e..ecda661 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/exporter_test.go @@ -43,7 +43,7 @@ type ( var ( identities = []string{"aaa", "bbb", "ccc"} votekeys = []string{"AAA", "BBB", "CCC"} - balances = map[string]float64{"aaa": 1, "bbb": 2, "ccc": 3} + balances = map[string]float64{"aaa": 1, "bbb": 2, "ccc": 3, "AAA": 4, "BBB": 5, "CCC": 6} identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"} nv = len(identities) staticEpochInfo = rpc.EpochInfo{ @@ -106,6 +106,16 @@ var ( staticLeaderSchedule = map[string][]int64{ "aaa": {0, 3, 6, 9, 12}, "bbb": {1, 4, 7, 10, 13}, "ccc": {2, 5, 8, 11, 14}, } + balanceMetricResponse = ` +# HELP solana_account_balance Solana account balances +# TYPE solana_account_balance gauge +solana_account_balance{address="AAA"} 4 +solana_account_balance{address="BBB"} 5 +solana_account_balance{address="CCC"} 6 +solana_account_balance{address="aaa"} 1 +solana_account_balance{address="bbb"} 2 +solana_account_balance{address="ccc"} 3 + ` ) /* @@ -394,9 +404,7 @@ func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases } func TestSolanaCollector_Collect_Static(t *testing.T) { - collector := createSolanaCollector( - &staticRPCClient{}, slotPacerSchedule, identities, []string{}, votekeys, identities, - ) + collector := NewSolanaCollector(&staticRPCClient{}, slotPacerSchedule, nil, identities, votekeys) prometheus.NewPedanticRegistry().MustRegister(collector) testCases := []collectionTest{ @@ -414,9 +422,9 @@ solana_active_validators{state="delinquent"} 1 ExpectedResponse: ` # HELP solana_validator_activated_stake Activated stake per validator # TYPE solana_validator_activated_stake gauge -solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49 -solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42 -solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43 +solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 49 +solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 42 +solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 43 `, }, { @@ -424,9 +432,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43 ExpectedResponse: ` # HELP solana_validator_last_vote Last voted slot per validator # TYPE solana_validator_last_vote gauge -solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92 -solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147 -solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148 +solana_validator_last_vote{nodekey="aaa",votekey="AAA"} 92 +solana_validator_last_vote{nodekey="bbb",votekey="BBB"} 147 +solana_validator_last_vote{nodekey="ccc",votekey="CCC"} 148 `, }, { @@ -434,9 +442,9 @@ solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148 ExpectedResponse: ` # HELP solana_validator_root_slot Root slot per validator # TYPE solana_validator_root_slot gauge -solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3 -solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18 -solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19 +solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 3 +solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 18 +solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 19 `, }, { @@ -444,9 +452,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19 ExpectedResponse: ` # HELP solana_validator_delinquent Whether a validator is delinquent # TYPE solana_validator_delinquent gauge -solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1 -solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 -solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0 +solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 1 +solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0 +solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 0 `, }, { @@ -458,14 +466,8 @@ solana_node_version{version="1.16.7"} 1 `, }, { - Name: "solana_account_balance", - ExpectedResponse: ` -# HELP solana_account_balance Solana account balances -# TYPE solana_account_balance gauge -solana_account_balance{address="aaa"} 1 -solana_account_balance{address="bbb"} 2 -solana_account_balance{address="ccc"} 3 - `, + Name: "solana_account_balance", + ExpectedResponse: balanceMetricResponse, }, } @@ -474,7 +476,7 @@ solana_account_balance{address="ccc"} 3 func TestSolanaCollector_Collect_Dynamic(t *testing.T) { client := newDynamicRPCClient() - collector := createSolanaCollector(client, slotPacerSchedule, identities, []string{}, votekeys, identities) + collector := NewSolanaCollector(client, slotPacerSchedule, nil, identities, votekeys) prometheus.NewPedanticRegistry().MustRegister(collector) // start off by testing initial state: @@ -493,9 +495,9 @@ solana_active_validators{state="delinquent"} 0 ExpectedResponse: ` # HELP solana_validator_activated_stake Activated stake per validator # TYPE solana_validator_activated_stake gauge -solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 1000000 -solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 1000000 -solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000 +solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 1000000 +solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 1000000 +solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000 `, }, { @@ -503,9 +505,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000 ExpectedResponse: ` # HELP solana_validator_root_slot Root slot per validator # TYPE solana_validator_root_slot gauge -solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0 -solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0 -solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0 +solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0 +solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0 +solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0 `, }, { @@ -513,9 +515,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0 ExpectedResponse: ` # HELP solana_validator_delinquent Whether a validator is delinquent # TYPE solana_validator_delinquent gauge -solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0 -solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 -solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0 +solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0 +solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0 +solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 0 `, }, { @@ -527,14 +529,8 @@ solana_node_version{version="v1.0.0"} 1 `, }, { - Name: "solana_account_balance", - ExpectedResponse: ` -# HELP solana_account_balance Solana account balances -# TYPE solana_account_balance gauge -solana_account_balance{address="aaa"} 1 -solana_account_balance{address="bbb"} 2 -solana_account_balance{address="ccc"} 3 -`, + Name: "solana_account_balance", + ExpectedResponse: balanceMetricResponse, }, } @@ -562,9 +558,9 @@ solana_active_validators{state="delinquent"} 1 ExpectedResponse: ` # HELP solana_validator_activated_stake Activated stake per validator # TYPE solana_validator_activated_stake gauge -solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 2000000 -solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 500000 -solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000 +solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 2000000 +solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 500000 +solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000 `, }, { @@ -572,9 +568,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000 ExpectedResponse: ` # HELP solana_validator_root_slot Root slot per validator # TYPE solana_validator_root_slot gauge -solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0 -solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0 -solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0 +solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0 +solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0 +solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0 `, }, { @@ -582,9 +578,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0 ExpectedResponse: ` # HELP solana_validator_delinquent Whether a validator is delinquent # TYPE solana_validator_delinquent gauge -solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0 -solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 -solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 1 +solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0 +solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0 +solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 1 `, }, { @@ -596,14 +592,8 @@ solana_node_version{version="v1.2.3"} 1 `, }, { - Name: "solana_account_balance", - ExpectedResponse: ` -# HELP solana_account_balance Solana account balances -# TYPE solana_account_balance gauge -solana_account_balance{address="aaa"} 1 -solana_account_balance{address="bbb"} 2 -solana_account_balance{address="ccc"} 3 -`, + Name: "solana_account_balance", + ExpectedResponse: balanceMetricResponse, }, } diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 6466158..d5f18f7 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -21,9 +21,9 @@ type SlotWatcher struct { client rpc.Provider // config: - leaderSlotAddresses []string - inflationRewardAddresses []string - feeRewardAddresses []string + nodekeys []string + votekeys []string + comprehensiveSlotTracking bool // currentEpoch is the current epoch we are watching currentEpoch int64 @@ -68,7 +68,7 @@ var ( Name: "solana_leader_slots_total", Help: "(DEPRECATED) Number of leader slots per leader, grouped by skip status", }, - []string{"status", "nodekey"}, + []string{SkipStatusLabel, NodekeyLabel}, ) leaderSlotsByEpoch = prometheus.NewCounterVec( @@ -76,7 +76,7 @@ var ( Name: "solana_leader_slots_by_epoch", Help: "Number of leader slots per leader, grouped by skip status and epoch", }, - []string{"status", "nodekey", "epoch"}, + []string{SkipStatusLabel, NodekeyLabel, EpochLabel}, ) inflationRewards = prometheus.NewGaugeVec( @@ -84,7 +84,7 @@ var ( Name: "solana_inflation_rewards", Help: "Inflation reward earned per validator vote account, per epoch", }, - []string{"votekey", "epoch"}, + []string{VotekeyLabel, EpochLabel}, ) feeRewards = prometheus.NewCounterVec( @@ -92,16 +92,18 @@ var ( Name: "solana_fee_rewards", Help: "Transaction fee rewards earned per validator identity account, per epoch", }, - []string{"nodekey", "epoch"}, + []string{NodekeyLabel, EpochLabel}, ) ) -func NewCollectorSlotWatcher(collector *solanaCollector) *SlotWatcher { +func NewSlotWatcher( + client rpc.Provider, nodekeys []string, votekeys []string, comprehensiveSlotTracking bool, +) *SlotWatcher { return &SlotWatcher{ - client: collector.rpcClient, - leaderSlotAddresses: collector.leaderSlotAddresses, - inflationRewardAddresses: collector.inflationRewardAddresses, - feeRewardAddresses: collector.feeRewardAddresses, + client: client, + nodekeys: nodekeys, + votekeys: votekeys, + comprehensiveSlotTracking: comprehensiveSlotTracking, } } @@ -157,8 +159,8 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) { } if epochInfo.Epoch > c.currentEpoch { - // if we have configured inflation reward addresses, fetch em - if len(c.inflationRewardAddresses) > 0 { + // fetch inflation rewards for vote accounts: + if len(c.votekeys) > 0 { err = c.fetchAndEmitInflationRewards(ctx, c.currentEpoch) if err != nil { klog.Errorf("Failed to emit inflation rewards, bailing out: %v", err) @@ -222,9 +224,7 @@ func (c *SlotWatcher) trackEpoch(ctx context.Context, epoch *rpc.EpochInfo) { ctx, cancel := context.WithTimeout(ctx, httpTimeout) defer cancel() klog.Infof("Updating leader schedule for epoch %v ...", c.currentEpoch) - leaderSchedule, err := GetTrimmedLeaderSchedule( - ctx, c.client, c.feeRewardAddresses, epoch.AbsoluteSlot, c.firstSlot, - ) + leaderSchedule, err := GetTrimmedLeaderSchedule(ctx, c.client, c.nodekeys, epoch.AbsoluteSlot, c.firstSlot) if err != nil { klog.Errorf("Failed to get trimmed leader schedule, bailing out: %v", err) } @@ -286,13 +286,13 @@ func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, endSlot i valid := float64(production.BlocksProduced) skipped := float64(production.LeaderSlots - production.BlocksProduced) - leaderSlotsTotal.WithLabelValues("valid", address).Add(valid) - leaderSlotsTotal.WithLabelValues("skipped", address).Add(skipped) + leaderSlotsTotal.WithLabelValues(StatusValid, address).Add(valid) + leaderSlotsTotal.WithLabelValues(StatusSkipped, address).Add(skipped) - if len(c.leaderSlotAddresses) == 0 || slices.Contains(c.leaderSlotAddresses, address) { + if slices.Contains(c.nodekeys, address) || c.comprehensiveSlotTracking { epochStr := toString(c.currentEpoch) - leaderSlotsByEpoch.WithLabelValues("valid", address, epochStr).Add(valid) - leaderSlotsByEpoch.WithLabelValues("skipped", address, epochStr).Add(skipped) + leaderSlotsByEpoch.WithLabelValues(StatusValid, address, epochStr).Add(valid) + leaderSlotsByEpoch.WithLabelValues(StatusSkipped, address, epochStr).Add(skipped) } } @@ -376,15 +376,13 @@ func (c *SlotWatcher) fetchAndEmitInflationRewards(ctx context.Context, epoch in ctx, cancel := context.WithTimeout(ctx, httpTimeout) defer cancel() - rewardInfos, err := c.client.GetInflationReward( - ctx, rpc.CommitmentConfirmed, c.inflationRewardAddresses, &epoch, nil, - ) + rewardInfos, err := c.client.GetInflationReward(ctx, rpc.CommitmentConfirmed, c.votekeys, &epoch, nil) if err != nil { return fmt.Errorf("error fetching inflation rewards: %w", err) } for i, rewardInfo := range rewardInfos { - address := c.inflationRewardAddresses[i] + address := c.votekeys[i] reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol) inflationRewards.WithLabelValues(address, toString(epoch)).Set(reward) } diff --git a/cmd/solana_exporter/slots_test.go b/cmd/solana_exporter/slots_test.go index daa3564..118c128 100644 --- a/cmd/solana_exporter/slots_test.go +++ b/cmd/solana_exporter/slots_test.go @@ -92,10 +92,9 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) { leaderSlotsTotal.Reset() leaderSlotsByEpoch.Reset() - collector := createSolanaCollector( - &staticRPCClient{}, 100*time.Millisecond, identities, []string{}, votekeys, identities, - ) - watcher := NewCollectorSlotWatcher(collector) + client := staticRPCClient{} + collector := NewSolanaCollector(&client, 100*time.Millisecond, nil, identities, votekeys) + watcher := NewSlotWatcher(&client, identities, votekeys, false) prometheus.NewPedanticRegistry().MustRegister(collector) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -163,8 +162,8 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { // create clients: client := newDynamicRPCClient() - collector := createSolanaCollector(client, 300*time.Millisecond, identities, []string{}, votekeys, identities) - watcher := NewCollectorSlotWatcher(collector) + collector := NewSolanaCollector(client, 300*time.Millisecond, nil, identities, votekeys) + watcher := NewSlotWatcher(client, identities, votekeys, false) prometheus.NewPedanticRegistry().MustRegister(collector) // start client/collector and wait a bit: diff --git a/cmd/solana_exporter/utils.go b/cmd/solana_exporter/utils.go index aaf06b9..492036f 100644 --- a/cmd/solana_exporter/utils.go +++ b/cmd/solana_exporter/utils.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/asymmetric-research/solana_exporter/pkg/rpc" "k8s.io/klog/v2" + "slices" ) func assertf(condition bool, format string, args ...any) { @@ -60,3 +61,55 @@ func GetTrimmedLeaderSchedule( return trimmedLeaderSchedule, nil } + +// GetAssociatedVoteAccounts returns the votekeys associated with a given list of nodekeys +func GetAssociatedVoteAccounts( + ctx context.Context, client rpc.Provider, commitment rpc.Commitment, nodekeys []string, +) ([]string, error) { + voteAccounts, err := client.GetVoteAccounts(ctx, commitment, nil) + if err != nil { + return nil, err + } + + // first map nodekey -> votekey: + voteAccountsMap := make(map[string]string) + for _, voteAccount := range append(voteAccounts.Current, voteAccounts.Delinquent...) { + voteAccountsMap[voteAccount.NodePubkey] = voteAccount.VotePubkey + } + + votekeys := make([]string, len(nodekeys)) + for i, nodeKey := range nodekeys { + votekey := voteAccountsMap[nodeKey] + if votekey == "" { + return nil, fmt.Errorf("failed to find vote key for node %v", nodeKey) + } + votekeys[i] = votekey + } + return votekeys, nil +} + +// FetchBalances fetches SOL balances for a list of addresses +func FetchBalances(ctx context.Context, client rpc.Provider, addresses []string) (map[string]float64, error) { + balances := make(map[string]float64) + for _, address := range addresses { + balance, err := client.GetBalance(ctx, rpc.CommitmentConfirmed, address) + if err != nil { + return nil, err + } + balances[address] = balance + } + return balances, nil +} + +// CombineUnique combines unique items from multiple arrays to a single array. +func CombineUnique[T comparable](args ...[]T) []T { + var uniqueItems []T + for _, arg := range args { + for _, item := range arg { + if !slices.Contains(uniqueItems, item) { + uniqueItems = append(uniqueItems, item) + } + } + } + return uniqueItems +} diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index 5925ae6..6b4fb33 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -22,3 +22,26 @@ func TestGetTrimmedLeaderSchedule(t *testing.T) { assert.Equal(t, map[string][]int64{"aaa": {10, 13, 16, 19, 22}, "bbb": {11, 14, 17, 20, 23}}, schedule) } + +func TestCombineUnique(t *testing.T) { + var ( + v1 = []string{"1", "2", "3"} + v2 = []string{"2", "3", "4"} + v3 = []string{"3", "4", "5"} + ) + + assert.Equal(t, []string{"1", "2", "3", "4", "5"}, CombineUnique(v1, v2, v3)) + assert.Equal(t, []string{"2", "3", "4", "5"}, CombineUnique(nil, v2, v3)) + assert.Equal(t, []string{"1", "2", "3", "4", "5"}, CombineUnique(v1, nil, v3)) + +} + +func TestFetchBalances(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client := staticRPCClient{} + fetchedBalances, err := FetchBalances(ctx, &client, CombineUnique(identities, votekeys)) + assert.NoError(t, err) + assert.Equal(t, balances, fetchedBalances) +}