diff --git a/Dockerfile b/Dockerfile index 8ce2836..8dafaae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.15 as builder +FROM golang:1.22 as builder COPY . /opt WORKDIR /opt diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go new file mode 100644 index 0000000..c0d88b5 --- /dev/null +++ b/cmd/solana_exporter/dynamic_test.go @@ -0,0 +1,246 @@ +package main + +import ( + "context" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestSolanaCollector_Collect_Dynamic(t *testing.T) { + client := newDynamicRPCClient() + collector := createSolanaCollector(client, slotPacerSchedule) + prometheus.NewPedanticRegistry().MustRegister(collector) + + // start off by testing initial state: + testCases := []collectionTest{ + { + Name: "solana_active_validators", + ExpectedResponse: ` +# HELP solana_active_validators Total number of active validators by state +# TYPE solana_active_validators gauge +solana_active_validators{state="current"} 3 +solana_active_validators{state="delinquent"} 0 +`, + }, + { + Name: "solana_validator_activated_stake", + 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 +`, + }, + { + Name: "solana_validator_root_slot", + 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 +`, + }, + { + Name: "solana_validator_delinquent", + 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 +`, + }, + { + Name: "solana_node_version", + ExpectedResponse: ` +# HELP solana_node_version Node version of solana +# TYPE solana_node_version gauge +solana_node_version{version="v1.0.0"} 1 +`, + }, + } + + runCollectionTests(t, collector, testCases) + + // now make some changes: + client.UpdateStake("aaa", 2_000_000) + client.UpdateStake("bbb", 500_000) + client.UpdateDelinquency("ccc", true) + client.UpdateVersion("v1.2.3") + + // now test the final state + testCases = []collectionTest{ + { + Name: "solana_active_validators", + ExpectedResponse: ` +# HELP solana_active_validators Total number of active validators by state +# TYPE solana_active_validators gauge +solana_active_validators{state="current"} 2 +solana_active_validators{state="delinquent"} 1 +`, + }, + { + Name: "solana_validator_activated_stake", + 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 +`, + }, + { + Name: "solana_validator_root_slot", + 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 +`, + }, + { + Name: "solana_validator_delinquent", + 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 +`, + }, + { + Name: "solana_node_version", + ExpectedResponse: ` +# HELP solana_node_version Node version of solana +# TYPE solana_node_version gauge +solana_node_version{version="v1.2.3"} 1 +`, + }, + } + + runCollectionTests(t, collector, testCases) +} + +type slotMetricValues struct { + SlotHeight float64 + TotalTransactions float64 + EpochNumber float64 + EpochFirstSlot float64 + EpochLastSlot float64 +} + +func getSlotMetricValues() slotMetricValues { + return slotMetricValues{ + SlotHeight: testutil.ToFloat64(confirmedSlotHeight), + TotalTransactions: testutil.ToFloat64(totalTransactionsTotal), + EpochNumber: testutil.ToFloat64(currentEpochNumber), + EpochFirstSlot: testutil.ToFloat64(epochFirstSlot), + EpochLastSlot: testutil.ToFloat64(epochLastSlot), + } +} + +func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { + // reset metrics before running tests: + leaderSlotsTotal.Reset() + leaderSlotsByEpoch.Reset() + + // create clients: + client := newDynamicRPCClient() + collector := createSolanaCollector(client, 300*time.Millisecond) + prometheus.NewPedanticRegistry().MustRegister(collector) + + // start client/collector and wait a bit: + runCtx, runCancel := context.WithCancel(context.Background()) + defer runCancel() + go client.Run(runCtx) + time.Sleep(time.Second) + + slotsCtx, slotsCancel := context.WithCancel(context.Background()) + defer slotsCancel() + go collector.WatchSlots(slotsCtx) + time.Sleep(time.Second) + + initial := getSlotMetricValues() + + // wait a bit: + var epochChanged bool + for i := 0; i < 5; i++ { + // wait a bit then get new metrics + time.Sleep(time.Second) + final := getSlotMetricValues() + + // make sure things are changing correctly: + assertSlotMetricsChangeCorrectly(t, initial, final) + + // sense check to make sure the exporter is not "ahead" of the client (due to double counting or whatever) + assert.LessOrEqualf( + t, + int(final.SlotHeight), + client.Slot, + "Exporter slot (%v) ahead of client slot (%v)!", + int(final.SlotHeight), + client.Slot, + ) + assert.LessOrEqualf( + t, + int(final.TotalTransactions), + client.TransactionCount, + "Exporter transaction count (%v) ahead of client transaction count (%v)!", + int(final.TotalTransactions), + client.TransactionCount, + ) + assert.LessOrEqualf( + t, + int(final.EpochNumber), + client.Epoch, + "Exporter epoch (%v) ahead of client epoch (%v)!", + int(final.EpochNumber), + client.Epoch, + ) + + // check if epoch changed + if final.EpochNumber > initial.EpochNumber { + epochChanged = true + } + + // make current final the new initial (for next iteration) + initial = final + } + + // epoch should have changed somewhere + assert.Truef(t, epochChanged, "Epoch has not changed!") +} + +func assertSlotMetricsChangeCorrectly(t *testing.T, initial slotMetricValues, final slotMetricValues) { + // make sure that things have increased + assert.Greaterf( + t, + final.SlotHeight, + initial.SlotHeight, + "Slot has not increased! (%v -> %v)", + initial.SlotHeight, + final.SlotHeight, + ) + assert.Greaterf( + t, + final.TotalTransactions, + initial.TotalTransactions, + "Total transactions have not increased! (%v -> %v)", + initial.TotalTransactions, + final.TotalTransactions, + ) + assert.GreaterOrEqualf( + t, + final.EpochNumber, + initial.EpochNumber, + "Epoch number has decreased! (%v -> %v)", + initial.EpochNumber, + final.EpochNumber, + ) +} diff --git a/cmd/solana_exporter/exporter.go b/cmd/solana_exporter/exporter.go index 5884b89..62a425f 100644 --- a/cmd/solana_exporter/exporter.go +++ b/cmd/solana_exporter/exporter.go @@ -26,6 +26,7 @@ func init() { type solanaCollector struct { rpcClient rpc.Provider + slotPace time.Duration totalValidatorsDesc *prometheus.Desc validatorActivatedStake *prometheus.Desc @@ -35,9 +36,10 @@ type solanaCollector struct { solanaVersion *prometheus.Desc } -func createSolanaCollector(provider rpc.Provider) *solanaCollector { +func createSolanaCollector(provider rpc.Provider, slotPace time.Duration) *solanaCollector { return &solanaCollector{ rpcClient: provider, + slotPace: slotPace, totalValidatorsDesc: prometheus.NewDesc( "solana_active_validators", "Total number of active validators by state", @@ -66,7 +68,7 @@ func createSolanaCollector(provider rpc.Provider) *solanaCollector { } func NewSolanaCollector(rpcAddr string) *solanaCollector { - return createSolanaCollector(rpc.NewRPCClient(rpcAddr)) + return createSolanaCollector(rpc.NewRPCClient(rpcAddr), slotPacerSchedule) } func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) { @@ -127,7 +129,7 @@ func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) { if err != nil { ch <- prometheus.NewInvalidMetric(c.solanaVersion, err) } else { - ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, *version) + ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version) } } @@ -142,7 +144,7 @@ func main() { collector := NewSolanaCollector(*rpcAddr) - go collector.WatchSlots() + go collector.WatchSlots(context.Background()) prometheus.MustRegister(collector) http.Handle("/metrics", promhttp.Handler()) diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go deleted file mode 100644 index 0dd0122..0000000 --- a/cmd/solana_exporter/exporter_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -var staticCollector = createSolanaCollector(&staticRPCClient{}) - -func TestSolanaCollector_Collect(t *testing.T) { - prometheus.NewPedanticRegistry().MustRegister(staticCollector) - - testCases := map[string]string{ - "solana_active_validators": ` -# HELP solana_active_validators Total number of active validators by state -# TYPE solana_active_validators gauge -solana_active_validators{state="current"} 2 -solana_active_validators{state="delinquent"} 1 -`, - "solana_validator_activated_stake": ` -# HELP solana_validator_activated_stake Activated stake per validator -# TYPE solana_validator_activated_stake gauge -solana_validator_activated_stake{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 49 -solana_validator_activated_stake{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 42 -solana_validator_activated_stake{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 43 -`, - "solana_validator_last_vote": ` -# HELP solana_validator_last_vote Last voted slot per validator -# TYPE solana_validator_last_vote gauge -solana_validator_last_vote{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 92 -solana_validator_last_vote{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 147 -solana_validator_last_vote{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 148 -`, - "solana_validator_root_slot": ` -# HELP solana_validator_root_slot Root slot per validator -# TYPE solana_validator_root_slot gauge -solana_validator_root_slot{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 3 -solana_validator_root_slot{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 18 -solana_validator_root_slot{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 19 -`, - "solana_validator_delinquent": ` -# HELP solana_validator_delinquent Whether a validator is delinquent -# TYPE solana_validator_delinquent gauge -solana_validator_delinquent{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 1 -solana_validator_delinquent{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0 -solana_validator_delinquent{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0 -`, - "solana_node_version": ` -# HELP solana_node_version Node version of solana -# TYPE solana_node_version gauge -solana_node_version{version="1.16.7"} 1 -`, - } - - for testName, expectedValue := range testCases { - t.Run( - testName, - func(t *testing.T) { - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedValue), - testName, - ); err != nil { - t.Errorf("unexpected collecting result for %s: \n%s", testName, err) - } - }, - ) - } -} - -func TestSolanaCollector_WatchSlots(t *testing.T) { - go staticCollector.WatchSlots() - time.Sleep(1 * time.Second) - - tests := []struct { - expectedValue float64 - metric prometheus.Gauge - }{ - { - expectedValue: float64(staticEpochInfo.AbsoluteSlot), - metric: confirmedSlotHeight, - }, - { - expectedValue: float64(staticEpochInfo.TransactionCount), - metric: totalTransactionsTotal, - }, - { - expectedValue: float64(staticEpochInfo.Epoch), - metric: currentEpochNumber, - }, - { - expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex), - metric: epochFirstSlot, - }, - { - expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex + staticEpochInfo.SlotsInEpoch), - metric: epochLastSlot, - }, - } - - for _, testCase := range tests { - name := extractName(testCase.metric.Desc()) - t.Run( - name, - func(t *testing.T) { - assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric)) - }, - ) - } - - hosts := []string{ - "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", - "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", - "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", - } - metrics := map[string]*prometheus.CounterVec{ - "solana_leader_slots_total": leaderSlotsTotal, - "solana_leader_slots_by_epoch": leaderSlotsByEpoch, - } - statuses := []string{"valid", "skipped"} - for name, metric := range metrics { - // subtest for each metric: - t.Run(name, func(t *testing.T) { - for _, status := range statuses { - // sub subtest for each status (as each one requires a different calc) - t.Run(status, func(t *testing.T) { - for _, host := range hosts { - testBlockProductionMetric(t, metric, host, status) - } - }) - } - }) - } -} - -func testBlockProductionMetric( - t *testing.T, - metric *prometheus.CounterVec, - host string, - status string, -) { - hostInfo := staticBlockProduction.Hosts[host] - // get expected value depending on status: - var expectedValue float64 - switch status { - case "valid": - expectedValue = float64(hostInfo.BlocksProduced) - case "skipped": - expectedValue = float64(hostInfo.LeaderSlots - hostInfo.BlocksProduced) - } - // get labels (leaderSlotsByEpoch requires an extra one) - labels := []string{status, host} - if metric == leaderSlotsByEpoch { - labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch)) - } - // now we can do the assertion: - assert.Equal(t, expectedValue, testutil.ToFloat64(metric.WithLabelValues(labels...))) -} diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 448db6c..fdf4d58 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -65,10 +65,10 @@ func init() { prometheus.MustRegister(leaderSlotsByEpoch) } -func (c *solanaCollector) WatchSlots() { +func (c *solanaCollector) WatchSlots(ctx context.Context) { // Get current slot height and epoch info - ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) - info, err := c.rpcClient.GetEpochInfo(ctx, rpc.CommitmentMax) + ctx_, cancel := context.WithTimeout(context.Background(), httpTimeout) + info, err := c.rpcClient.GetEpochInfo(ctx_, rpc.CommitmentMax) if err != nil { klog.Fatalf("failed to fetch epoch info, bailing out: %v", err) } @@ -89,44 +89,78 @@ func (c *solanaCollector) WatchSlots() { if err != nil { klog.Error(err) } - ticker := time.NewTicker(slotPacerSchedule) + ticker := time.NewTicker(c.slotPace) for { - <-ticker.C + select { + case <-ctx.Done(): + klog.Infof("Stopping WatchSlots() at slot %v", watermark) + return - // Get current slot height and epoch info - ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) - info, err := c.rpcClient.GetEpochInfo(ctx, rpc.CommitmentMax) - if err != nil { - klog.Infof("failed to fetch epoch info, retrying: %v", err) + default: + <-ticker.C + + // Get current slot height and epoch info + ctx_, cancel := context.WithTimeout(context.Background(), httpTimeout) + info, err := c.rpcClient.GetEpochInfo(ctx_, rpc.CommitmentMax) + if err != nil { + klog.Warningf("failed to fetch epoch info, retrying: %v", err) + cancel() + continue + } cancel() - continue - } - cancel() - if watermark == info.AbsoluteSlot { - klog.Infof("slot has not advanced at %d, skipping", info.AbsoluteSlot) - continue - } + if watermark == info.AbsoluteSlot { + klog.V(2).Infof("slot has not advanced at %d, skipping", info.AbsoluteSlot) + continue + } - if currentEpoch != info.Epoch { - klog.Infof( - "changing epoch from %d to %d. Watermark: %d, lastSlot: %d", - currentEpoch, - info.Epoch, - watermark, - lastSlot, - ) + if currentEpoch != info.Epoch { + klog.Infof( + "changing epoch from %d to %d. Watermark: %d, lastSlot: %d", + currentEpoch, + info.Epoch, + watermark, + lastSlot, + ) + + last, err := updateCounters(c.rpcClient, currentEpoch, watermark, &lastSlot) + if err != nil { + klog.Error(err) + continue + } + + klog.Infof( + "counters updated to slot %d (+%d), epoch %d (slots %d-%d, %d remaining)", + last, + last-watermark, + currentEpoch, + firstSlot, + lastSlot, + lastSlot-last, + ) + + watermark = last + currentEpoch, firstSlot, lastSlot = getEpochBounds(info) + + currentEpochNumber.Set(float64(currentEpoch)) + epochFirstSlot.Set(float64(firstSlot)) + epochLastSlot.Set(float64(lastSlot)) + } - last, err := updateCounters(c.rpcClient, currentEpoch, watermark, &lastSlot) + totalTransactionsTotal.Set(float64(info.TransactionCount)) + confirmedSlotHeight.Set(float64(info.AbsoluteSlot)) + + last, err := updateCounters(c.rpcClient, currentEpoch, watermark, nil) if err != nil { - klog.Error(err) + klog.Info(err) continue } klog.Infof( - "counters updated to slot %d (+%d), epoch %d (slots %d-%d, %d remaining)", + "counters updated to slot %d (offset %d, +%d), epoch %d (slots %d-%d, %d remaining)", last, + info.SlotIndex, last-watermark, currentEpoch, firstSlot, @@ -135,34 +169,7 @@ func (c *solanaCollector) WatchSlots() { ) watermark = last - currentEpoch, firstSlot, lastSlot = getEpochBounds(info) - - currentEpochNumber.Set(float64(currentEpoch)) - epochFirstSlot.Set(float64(firstSlot)) - epochLastSlot.Set(float64(lastSlot)) } - - totalTransactionsTotal.Set(float64(info.TransactionCount)) - confirmedSlotHeight.Set(float64(info.AbsoluteSlot)) - - last, err := updateCounters(c.rpcClient, currentEpoch, watermark, nil) - if err != nil { - klog.Info(err) - continue - } - - klog.Infof( - "counters updated to slot %d (offset %d, +%d), epoch %d (slots %d-%d, %d remaining)", - last, - info.SlotIndex, - last-watermark, - currentEpoch, - firstSlot, - lastSlot, - lastSlot-last, - ) - - watermark = last } } diff --git a/cmd/solana_exporter/static_test.go b/cmd/solana_exporter/static_test.go new file mode 100644 index 0000000..e9dd6c8 --- /dev/null +++ b/cmd/solana_exporter/static_test.go @@ -0,0 +1,166 @@ +package main + +import ( + "context" + "fmt" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestSolanaCollector_Collect_Static(t *testing.T) { + collector := createSolanaCollector( + &staticRPCClient{}, + slotPacerSchedule, + ) + prometheus.NewPedanticRegistry().MustRegister(collector) + + testCases := []collectionTest{ + { + Name: "solana_active_validators", + ExpectedResponse: ` +# HELP solana_active_validators Total number of active validators by state +# TYPE solana_active_validators gauge +solana_active_validators{state="current"} 2 +solana_active_validators{state="delinquent"} 1 +`, + }, + { + Name: "solana_validator_activated_stake", + 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 +`, + }, + { + Name: "solana_validator_last_vote", + 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 +`, + }, + { + Name: "solana_validator_root_slot", + 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 +`, + }, + { + Name: "solana_validator_delinquent", + 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 +`, + }, + { + Name: "solana_node_version", + ExpectedResponse: ` +# HELP solana_node_version Node version of solana +# TYPE solana_node_version gauge +solana_node_version{version="1.16.7"} 1 +`, + }, + } + + runCollectionTests(t, collector, testCases) +} + +func TestSolanaCollector_WatchSlots_Static(t *testing.T) { + // reset metrics before running tests: + leaderSlotsTotal.Reset() + leaderSlotsByEpoch.Reset() + + collector := createSolanaCollector( + &staticRPCClient{}, + 100*time.Millisecond, + ) + prometheus.NewPedanticRegistry().MustRegister(collector) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go collector.WatchSlots(ctx) + time.Sleep(1 * time.Second) + + firstSlot := staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex + lastSlot := firstSlot + staticEpochInfo.SlotsInEpoch + tests := []struct { + expectedValue float64 + metric prometheus.Gauge + }{ + {expectedValue: float64(staticEpochInfo.AbsoluteSlot), metric: confirmedSlotHeight}, + {expectedValue: float64(staticEpochInfo.TransactionCount), metric: totalTransactionsTotal}, + {expectedValue: float64(staticEpochInfo.Epoch), metric: currentEpochNumber}, + {expectedValue: float64(firstSlot), metric: epochFirstSlot}, + {expectedValue: float64(lastSlot), metric: epochLastSlot}, + } + + for _, testCase := range tests { + name := extractName(testCase.metric.Desc()) + t.Run(name, func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric)) + }) + } + + metrics := map[string]*prometheus.CounterVec{ + "solana_leader_slots_total": leaderSlotsTotal, + "solana_leader_slots_by_epoch": leaderSlotsByEpoch, + } + statuses := []string{"valid", "skipped"} + for name, metric := range metrics { + // subtest for each metric: + t.Run(name, func(t *testing.T) { + for _, status := range statuses { + // sub subtest for each status (as each one requires a different calc) + t.Run(status, func(t *testing.T) { + for _, identity := range identities { + testBlockProductionMetric(t, metric, identity, status) + } + }) + } + }) + } +} + +func testBlockProductionMetric( + t *testing.T, + metric *prometheus.CounterVec, + host string, + status string, +) { + hostInfo := staticBlockProduction.Hosts[host] + // get expected value depending on status: + var expectedValue float64 + switch status { + case "valid": + expectedValue = float64(hostInfo.BlocksProduced) + case "skipped": + expectedValue = float64(hostInfo.LeaderSlots - hostInfo.BlocksProduced) + } + // get labels (leaderSlotsByEpoch requires an extra one) + labels := []string{status, host} + if metric == leaderSlotsByEpoch { + labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch)) + } + // now we can do the assertion: + assert.Equalf( + t, + expectedValue, + testutil.ToFloat64(metric.WithLabelValues(labels...)), + "wrong value for block-production metric with labels: %s", + labels, + ) +} diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index c84870e..03d5486 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -1,18 +1,48 @@ package main import ( + "bytes" "context" "github.com/certusone/solana_exporter/pkg/rpc" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "math/rand" "regexp" + "testing" + "time" ) type ( - staticRPCClient struct{} - // TODO: create dynamicRPCClient + according tests! + staticRPCClient struct{} + dynamicRPCClient struct { + Slot int + BlockHeight int + Epoch int + EpochSize int + SlotTime time.Duration + TransactionCount int + Version string + SlotInfos map[int]slotInfo + LeaderIndex int + ValidatorInfos map[string]validatorInfo + } + slotInfo struct { + leader string + blockProduced bool + } + validatorInfo struct { + Stake int + LastVote int + Commission int + Delinquent bool + } ) var ( + identities = []string{"aaa", "bbb", "ccc"} + identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"} + nv = len(identities) staticEpochInfo = rpc.EpochInfo{ AbsoluteSlot: 166598, BlockHeight: 166500, @@ -22,47 +52,15 @@ var ( TransactionCount: 22661093, } staticBlockProduction = rpc.BlockProduction{ - FirstSlot: 1000, - LastSlot: 2000, + FirstSlot: 100000000, + LastSlot: 200000000, Hosts: map[string]rpc.BlockProductionPerHost{ - "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { - LeaderSlots: 400, - BlocksProduced: 360, - }, - "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { - LeaderSlots: 300, - BlocksProduced: 296, - }, - "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae": { - LeaderSlots: 300, - BlocksProduced: 0, - }, + "bbb": {LeaderSlots: 40000000, BlocksProduced: 36000000}, + "ccc": {LeaderSlots: 30000000, BlocksProduced: 29600000}, + "aaa": {LeaderSlots: 30000000, BlocksProduced: 10000000}, }, } -) - -//goland:noinspection GoUnusedParameter -func (c *staticRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) { - return &staticEpochInfo, nil -} - -//goland:noinspection GoUnusedParameter -func (c *staticRPCClient) GetSlot(ctx context.Context) (int64, error) { - return staticEpochInfo.AbsoluteSlot, nil -} - -//goland:noinspection GoUnusedParameter -func (c *staticRPCClient) GetVersion(ctx context.Context) (*string, error) { - version := "1.16.7" - return &version, nil -} - -//goland:noinspection GoUnusedParameter -func (c *staticRPCClient) GetVoteAccounts( - ctx context.Context, - params []interface{}, -) (*rpc.VoteAccounts, error) { - voteAccounts := rpc.VoteAccounts{ + staticVoteAccounts = rpc.VoteAccounts{ Current: []rpc.VoteAccount{ { ActivatedStake: 42, @@ -73,9 +71,9 @@ func (c *staticRPCClient) GetVoteAccounts( }, EpochVoteAccount: true, LastVote: 147, - NodePubkey: "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", + NodePubkey: "bbb", RootSlot: 18, - VotePubkey: "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw", + VotePubkey: "BBB", }, { ActivatedStake: 43, @@ -86,9 +84,9 @@ func (c *staticRPCClient) GetVoteAccounts( }, EpochVoteAccount: true, LastVote: 148, - NodePubkey: "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", + NodePubkey: "ccc", RootSlot: 19, - VotePubkey: "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw", + VotePubkey: "CCC", }, }, Delinquent: []rpc.VoteAccount{ @@ -101,13 +99,40 @@ func (c *staticRPCClient) GetVoteAccounts( }, EpochVoteAccount: true, LastVote: 92, - NodePubkey: "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", + NodePubkey: "aaa", RootSlot: 3, - VotePubkey: "xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU", + VotePubkey: "AAA", }, }, } - return &voteAccounts, nil +) + +/* +===== STATIC CLIENT =====: +*/ + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) { + return &staticEpochInfo, nil +} + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetSlot(ctx context.Context) (int64, error) { + return staticEpochInfo.AbsoluteSlot, nil +} + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetVersion(ctx context.Context) (string, error) { + version := "1.16.7" + return version, nil +} + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetVoteAccounts( + ctx context.Context, + params []interface{}, +) (*rpc.VoteAccounts, error) { + return &staticVoteAccounts, nil } //goland:noinspection GoUnusedParameter @@ -119,14 +144,192 @@ func (c *staticRPCClient) GetBlockProduction( return staticBlockProduction, nil } +/* +===== DYNAMIC CLIENT =====: +*/ + +func newDynamicRPCClient() *dynamicRPCClient { + validatorInfos := make(map[string]validatorInfo) + for identity := range identityVotes { + validatorInfos[identity] = validatorInfo{ + Stake: 1_000_000, + LastVote: 0, + Commission: 5, + Delinquent: false, + } + } + return &dynamicRPCClient{ + Slot: 0, + BlockHeight: 0, + Epoch: 0, + EpochSize: 20, + SlotTime: 100 * time.Millisecond, + TransactionCount: 0, + Version: "v1.0.0", + SlotInfos: map[int]slotInfo{}, + LeaderIndex: 0, + ValidatorInfos: validatorInfos, + } +} + +func (c *dynamicRPCClient) Run(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + + default: + c.newSlot() + // add 5% noise to the slot time: + noiseRange := float64(c.SlotTime) * 0.05 + noise := (rand.Float64()*2 - 1) * noiseRange + time.Sleep(c.SlotTime + time.Duration(noise)) + } + } +} + +func (c *dynamicRPCClient) newSlot() { + c.Slot++ + + // leader changes every 4 slots + if c.Slot%4 == 0 { + c.LeaderIndex = (c.LeaderIndex + 1) % nv + } + + if c.Slot%c.EpochSize == 0 { + c.Epoch++ + } + + // assume 90% chance of block produced: + blockProduced := rand.Intn(100) <= 90 + // add slot info: + c.SlotInfos[c.Slot] = slotInfo{ + leader: identities[c.LeaderIndex], + blockProduced: blockProduced, + } + + if blockProduced { + c.BlockHeight++ + // only add some transactions if a block was produced + c.TransactionCount += rand.Intn(10) + // assume both other validators voted + for i := 1; i < 3; i++ { + otherValidatorIndex := (c.LeaderIndex + i) % nv + identity := identities[otherValidatorIndex] + info := c.ValidatorInfos[identity] + info.LastVote = c.Slot + c.ValidatorInfos[identity] = info + } + } +} + +func (c *dynamicRPCClient) UpdateVersion(version string) { + c.Version = version +} + +func (c *dynamicRPCClient) UpdateStake(validator string, amount int) { + info := c.ValidatorInfos[validator] + info.Stake = amount + c.ValidatorInfos[validator] = info +} + +func (c *dynamicRPCClient) UpdateCommission(validator string, newCommission int) { + info := c.ValidatorInfos[validator] + info.Commission = newCommission + c.ValidatorInfos[validator] = info +} + +func (c *dynamicRPCClient) UpdateDelinquency(validator string, newDelinquent bool) { + info := c.ValidatorInfos[validator] + info.Delinquent = newDelinquent + c.ValidatorInfos[validator] = info +} + +//goland:noinspection GoUnusedParameter +func (c *dynamicRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) { + return &rpc.EpochInfo{ + AbsoluteSlot: int64(c.Slot), + BlockHeight: int64(c.BlockHeight), + Epoch: int64(c.Epoch), + SlotIndex: int64(c.Slot % c.EpochSize), + SlotsInEpoch: int64(c.EpochSize), + TransactionCount: int64(c.TransactionCount), + }, nil +} + +//goland:noinspection GoUnusedParameter +func (c *dynamicRPCClient) GetSlot(ctx context.Context) (int64, error) { + return int64(c.Slot), nil +} + +//goland:noinspection GoUnusedParameter +func (c *dynamicRPCClient) GetVersion(ctx context.Context) (string, error) { + return c.Version, nil +} + +//goland:noinspection GoUnusedParameter +func (c *dynamicRPCClient) GetVoteAccounts( + ctx context.Context, + params []interface{}, +) (*rpc.VoteAccounts, error) { + var currentVoteAccounts, delinquentVoteAccounts []rpc.VoteAccount + for identity, vote := range identityVotes { + info := c.ValidatorInfos[identity] + voteAccount := rpc.VoteAccount{ + ActivatedStake: int64(info.Stake), + Commission: info.Commission, + EpochCredits: [][]int{}, + EpochVoteAccount: true, + LastVote: info.LastVote, + NodePubkey: identity, + RootSlot: 0, + VotePubkey: vote, + } + if info.Delinquent { + delinquentVoteAccounts = append(delinquentVoteAccounts, voteAccount) + } else { + currentVoteAccounts = append(currentVoteAccounts, voteAccount) + } + } + return &rpc.VoteAccounts{Current: currentVoteAccounts, Delinquent: delinquentVoteAccounts}, nil +} + +//goland:noinspection GoUnusedParameter +func (c *dynamicRPCClient) GetBlockProduction( + ctx context.Context, + firstSlot *int64, + lastSlot *int64, +) (rpc.BlockProduction, error) { + hostProduction := make(map[string]rpc.BlockProductionPerHost) + for _, identity := range identities { + hostProduction[identity] = rpc.BlockProductionPerHost{LeaderSlots: 0, BlocksProduced: 0} + } + for i := *firstSlot; i <= *lastSlot; i++ { + info := c.SlotInfos[int(i)] + hp := hostProduction[info.leader] + hp.LeaderSlots++ + if info.blockProduced { + hp.BlocksProduced++ + } + hostProduction[info.leader] = hp + } + return rpc.BlockProduction{ + FirstSlot: *firstSlot, + LastSlot: *lastSlot, + Hosts: hostProduction, + }, nil +} + +/* +===== OTHER TEST UTILITIES =====: +*/ + // extractName takes a Prometheus descriptor and returns its name func extractName(desc *prometheus.Desc) string { // Get the string representation of the descriptor descString := desc.String() - // Use regex to extract the metric name and help message from the descriptor string reName := regexp.MustCompile(`fqName: "([^"]+)"`) - nameMatch := reName.FindStringSubmatch(descString) var name string @@ -136,3 +339,17 @@ func extractName(desc *prometheus.Desc) string { return name } + +type collectionTest struct { + Name string + ExpectedResponse string +} + +func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases []collectionTest) { + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + err := testutil.CollectAndCompare(collector, bytes.NewBufferString(test.ExpectedResponse), test.Name) + assert.Nilf(t, "unexpected collecting result for %s: \n%s", test.Name, err) + }) + } +} diff --git a/go.mod b/go.mod index 66711a2..ba6964c 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,21 @@ module github.com/certusone/solana_exporter -go 1.13 +go 1.22 require ( - github.com/prometheus/client_golang v1.4.0 - github.com/prometheus/common v0.9.1 - github.com/stretchr/testify v1.4.0 - k8s.io/klog/v2 v2.4.0 + github.com/prometheus/client_golang v1.19.1 + github.com/stretchr/testify v1.9.0 + k8s.io/klog/v2 v2.120.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/sys v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index c509d19..c40adbc 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,15 +18,20 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -52,17 +58,22 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0 h1:YVIb/fVcOTMSqtqZWSKnHpSLBxu8DKgxq8z6RuBZwqI= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -71,6 +82,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -84,9 +97,11 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= @@ -96,5 +111,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index ba2ed70..0aa9d9a 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -63,7 +63,7 @@ type Provider interface { // GetVersion retrieves the version of the Solana node. // The method takes a context for cancellation. // It returns a pointer to a string containing the version information, or an error if the operation fails. - GetVersion(ctx context.Context) (*string, error) + GetVersion(ctx context.Context) (string, error) } func (c Commitment) MarshalJSON() ([]byte, error) { diff --git a/pkg/rpc/version.go b/pkg/rpc/version.go index abfe1b9..048c233 100644 --- a/pkg/rpc/version.go +++ b/pkg/rpc/version.go @@ -17,27 +17,27 @@ type ( } ) -func (c *Client) GetVersion(ctx context.Context) (*string, error) { +func (c *Client) GetVersion(ctx context.Context) (string, error) { body, err := c.rpcRequest(ctx, formatRPCRequest("getVersion", []interface{}{})) if body == nil { - return nil, fmt.Errorf("RPC call failed: Body empty") + return "", fmt.Errorf("RPC call failed: Body empty") } if err != nil { - return nil, fmt.Errorf("RPC call failed: %w", err) + return "", fmt.Errorf("RPC call failed: %w", err) } klog.V(2).Infof("version response: %v", string(body)) var resp GetVersionResponse if err = json.Unmarshal(body, &resp); err != nil { - return nil, fmt.Errorf("failed to decode response body: %w", err) + return "", fmt.Errorf("failed to decode response body: %w", err) } if resp.Error.Code != 0 { - return nil, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message) + return "", fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message) } - return &resp.Result.Version, nil + return resp.Result.Version, nil }