diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go deleted file mode 100644 index c0d88b5..0000000 --- a/cmd/solana_exporter/dynamic_test.go +++ /dev/null @@ -1,246 +0,0 @@ -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/utils_test.go b/cmd/solana_exporter/exporter_test.go similarity index 58% rename from cmd/solana_exporter/utils_test.go rename to cmd/solana_exporter/exporter_test.go index 03d5486..d638c62 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/exporter_test.go @@ -140,8 +140,8 @@ func (c *staticRPCClient) GetBlockProduction( ctx context.Context, firstSlot *int64, lastSlot *int64, -) (rpc.BlockProduction, error) { - return staticBlockProduction, nil +) (*rpc.BlockProduction, error) { + return &staticBlockProduction, nil } /* @@ -299,7 +299,7 @@ func (c *dynamicRPCClient) GetBlockProduction( ctx context.Context, firstSlot *int64, lastSlot *int64, -) (rpc.BlockProduction, error) { +) (*rpc.BlockProduction, error) { hostProduction := make(map[string]rpc.BlockProductionPerHost) for _, identity := range identities { hostProduction[identity] = rpc.BlockProductionPerHost{LeaderSlots: 0, BlocksProduced: 0} @@ -313,11 +313,12 @@ func (c *dynamicRPCClient) GetBlockProduction( } hostProduction[info.leader] = hp } - return rpc.BlockProduction{ + production := rpc.BlockProduction{ FirstSlot: *firstSlot, LastSlot: *lastSlot, Hosts: hostProduction, - }, nil + } + return &production, nil } /* @@ -349,7 +350,195 @@ func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases 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) + assert.Nilf(t, err, "unexpected collecting result for %s: \n%s", test.Name, err) }) } } + +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_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) +} diff --git a/cmd/solana_exporter/slots_test.go b/cmd/solana_exporter/slots_test.go new file mode 100644 index 0000000..db0cf5c --- /dev/null +++ b/cmd/solana_exporter/slots_test.go @@ -0,0 +1,214 @@ +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" +) + +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 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, + ) +} + +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, + ) +} + +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 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!") +} diff --git a/cmd/solana_exporter/static_test.go b/cmd/solana_exporter/static_test.go deleted file mode 100644 index e9dd6c8..0000000 --- a/cmd/solana_exporter/static_test.go +++ /dev/null @@ -1,166 +0,0 @@ -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/go.mod b/go.mod index ba6964c..64bb422 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,12 @@ require ( 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/kr/text v0.2.0 // 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 + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c40adbc..07ec9eb 100644 --- a/go.sum +++ b/go.sum @@ -1,119 +1,40 @@ -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -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= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -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 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 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 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 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 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 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= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 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= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -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 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 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/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= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/blockproduction.go b/pkg/rpc/blockproduction.go deleted file mode 100644 index 1c1c7bd..0000000 --- a/pkg/rpc/blockproduction.go +++ /dev/null @@ -1,94 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "fmt" - - "k8s.io/klog/v2" -) - -type ( - getBlockProductionConfig struct { - Range getBlockProductionRange `json:"range,omitempty"` - } - - getBlockProductionRange struct { - FirstSlot int64 `json:"firstSlot"` - LastSlot *int64 `json:"lastSlot,omitempty"` - } - - getBlockProductionValue struct { - ByIdentity map[string][]int64 `json:"byIdentity"` - Range getBlockProductionRange `json:"range"` - } - - getBlockProductionResult struct { - Value getBlockProductionValue `json:"value"` - } - - getBlockProductionResponse struct { - Result getBlockProductionResult `json:"result"` - Error rpcError2 `json:"error"` - } - - BlockProductionPerHost struct { - LeaderSlots int64 - BlocksProduced int64 - } - - BlockProduction struct { - FirstSlot int64 - LastSlot int64 - Hosts map[string]BlockProductionPerHost - } -) - -// https://solana.com/docs/rpc/http/getblockproduction -func (c *Client) GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (BlockProduction, error) { - config := make([]interface{}, 0, 1) - if firstSlot != nil { - config = append(config, - getBlockProductionConfig{ - Range: getBlockProductionRange{ - FirstSlot: *firstSlot, - LastSlot: lastSlot, - }, - }) - } - - ret := BlockProduction{ - FirstSlot: 0, - LastSlot: 0, - Hosts: nil, - } - - body, err := c.rpcRequest(ctx, formatRPCRequest("getBlockProduction", config)) - if err != nil { - return ret, fmt.Errorf("RPC call failed: %w", err) - } - - klog.V(2).Infof("getBlockProduction response: %v", string(body)) - - var resp getBlockProductionResponse - if err = json.Unmarshal(body, &resp); err != nil { - return ret, fmt.Errorf("failed to decode response body: %w", err) - } - - if resp.Error.Code != 0 { - return ret, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message) - } - - ret.FirstSlot = resp.Result.Value.Range.FirstSlot - ret.LastSlot = *resp.Result.Value.Range.LastSlot - ret.Hosts = make(map[string]BlockProductionPerHost) - - for id, arr := range resp.Result.Value.ByIdentity { - ret.Hosts[id] = BlockProductionPerHost{ - LeaderSlots: arr[0], - BlocksProduced: arr[1], - } - } - - return ret, nil -} diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index 0aa9d9a..0994823 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "k8s.io/klog/v2" "net/http" @@ -15,12 +16,7 @@ type ( rpcAddr string } - rpcError1 struct { - Message string `json:"message"` - Code int64 `json:"id"` - } - - rpcError2 struct { // TODO: combine these error types into a single one + rpcError struct { Message string `json:"message"` Code int64 `json:"code"` } @@ -42,7 +38,7 @@ type Provider interface { // GetBlockProduction retrieves the block production information for the specified slot range. // The method takes a context for cancellation, and pointers to the first and last slots of the range. // It returns a BlockProduction struct containing the block production details, or an error if the operation fails. - GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (BlockProduction, error) + GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (*BlockProduction, error) // GetEpochInfo retrieves the information regarding the current epoch. // The method takes a context for cancellation and a commitment level to specify the desired state. @@ -56,13 +52,13 @@ type Provider interface { // GetVoteAccounts retrieves the vote accounts information. // The method takes a context for cancellation and a slice of parameters to filter the vote accounts. - // It returns a pointer to a GetVoteAccountsResponse struct containing the vote accounts details, + // It returns a pointer to a VoteAccounts struct containing the vote accounts details, // or an error if the operation fails. GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) // 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. + // It returns a string containing the version information, or an error if the operation fails. GetVersion(ctx context.Context) (string, error) } @@ -82,29 +78,29 @@ const ( ) func NewRPCClient(rpcAddr string) *Client { - c := &Client{ + client := &Client{ httpClient: http.Client{}, rpcAddr: rpcAddr, } - return c + return client } func formatRPCRequest(method string, params []interface{}) io.Reader { - r := &rpcRequest{ + request := &rpcRequest{ Version: "2.0", ID: 1, Method: method, Params: params, } - b, err := json.Marshal(r) + buffer, err := json.Marshal(request) if err != nil { panic(err) } - klog.V(2).Infof("jsonrpc request: %s", string(b)) - return bytes.NewBuffer(b) + klog.V(2).Infof("jsonrpc request: %s", string(buffer)) + return bytes.NewBuffer(buffer) } func (c *Client) rpcRequest(ctx context.Context, data io.Reader) ([]byte, error) { @@ -128,3 +124,92 @@ func (c *Client) rpcRequest(ctx context.Context, data io.Reader) ([]byte, error) return body, nil } + +func (c *Client) getResponse(ctx context.Context, method string, params []interface{}, result HasRPCError) error { + body, err := c.rpcRequest(ctx, formatRPCRequest(method, params)) + // check if there was an error making the request: + if err != nil { + return fmt.Errorf("%s RPC call failed: %w", method, err) + } + // log response: + klog.V(2).Infof("%s response: %v", method, string(body)) + + // unmarshal the response into the predicted format + if err = json.Unmarshal(body, result); err != nil { + return fmt.Errorf("failed to decode %s response body: %w", method, err) + } + + if result.getError().Code != 0 { + return fmt.Errorf("RPC error: %d %v", result.getError().Code, result.getError().Message) + } + + return nil +} + +func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) { + var resp response[EpochInfo] + if err := c.getResponse(ctx, "getEpochInfo", []interface{}{commitment}, &resp); err != nil { + return nil, err + } + return &resp.Result, nil +} + +func (c *Client) GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) { + var resp response[VoteAccounts] + if err := c.getResponse(ctx, "getVoteAccounts", params, &resp); err != nil { + return nil, err + } + return &resp.Result, nil +} + +func (c *Client) GetVersion(ctx context.Context) (string, error) { + var resp response[struct { + Version string `json:"solana-core"` + }] + if err := c.getResponse(ctx, "getVersion", []interface{}{}, &resp); err != nil { + return "", err + } + return resp.Result.Version, nil +} + +func (c *Client) GetSlot(ctx context.Context) (int64, error) { + var resp response[int64] + if err := c.getResponse(ctx, "getSlot", []interface{}{}, &resp); err != nil { + return 0, err + } + return resp.Result, nil +} + +func (c *Client) GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (*BlockProduction, error) { + // format params: + params := make([]interface{}, 1) + if firstSlot != nil { + params[0] = map[string]interface{}{ + "range": blockProductionRange{ + FirstSlot: *firstSlot, + LastSlot: lastSlot, + }, + } + } + + // make request: + var resp response[blockProductionResult] + if err := c.getResponse(ctx, "getBlockProduction", params, &resp); err != nil { + return nil, err + } + + // convert to BlockProduction format: + hosts := make(map[string]BlockProductionPerHost) + for id, arr := range resp.Result.Value.ByIdentity { + hosts[id] = BlockProductionPerHost{ + LeaderSlots: arr[0], + BlocksProduced: arr[1], + } + } + production := BlockProduction{ + FirstSlot: resp.Result.Value.Range.FirstSlot, + LastSlot: *resp.Result.Value.Range.LastSlot, + Hosts: hosts, + } + return &production, nil +} diff --git a/pkg/rpc/epochinfo.go b/pkg/rpc/epochinfo.go deleted file mode 100644 index de0369c..0000000 --- a/pkg/rpc/epochinfo.go +++ /dev/null @@ -1,51 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "fmt" - "k8s.io/klog/v2" -) - -type ( - EpochInfo struct { - // Current absolute slot in epoch - AbsoluteSlot int64 `json:"absoluteSlot"` - // Current block height - BlockHeight int64 `json:"blockHeight"` - // Current epoch number - Epoch int64 `json:"epoch"` - // Current slot relative to the start of the current epoch - SlotIndex int64 `json:"slotIndex"` - // Number of slots in this epoch - SlotsInEpoch int64 `json:"slotsInEpoch"` - // Total number of transactions ever (?) - TransactionCount int64 `json:"transactionCount"` - } - - GetEpochInfoResponse struct { - Result EpochInfo `json:"result"` - Error rpcError1 `json:"error"` - } -) - -// https://docs.solana.com/developing/clients/jsonrpc-api#getepochinfo -func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) { - body, err := c.rpcRequest(ctx, formatRPCRequest("getEpochInfo", []interface{}{commitment})) - if err != nil { - return nil, fmt.Errorf("RPC call failed: %w", err) - } - - klog.V(2).Infof("epoch info response: %v", string(body)) - - var resp GetEpochInfoResponse - if err = json.Unmarshal(body, &resp); err != nil { - return nil, 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 &resp.Result, nil -} diff --git a/pkg/rpc/responses.go b/pkg/rpc/responses.go new file mode 100644 index 0000000..926f1d3 --- /dev/null +++ b/pkg/rpc/responses.go @@ -0,0 +1,70 @@ +package rpc + +type ( + response[T any] struct { + Result T `json:"result"` + Error rpcError `json:"error"` + } + + EpochInfo struct { + // Current absolute slot in epoch + AbsoluteSlot int64 `json:"absoluteSlot"` + // Current block height + BlockHeight int64 `json:"blockHeight"` + // Current epoch number + Epoch int64 `json:"epoch"` + // Current slot relative to the start of the current epoch + SlotIndex int64 `json:"slotIndex"` + // Number of slots in this epoch + SlotsInEpoch int64 `json:"slotsInEpoch"` + // Total number of transactions + TransactionCount int64 `json:"transactionCount"` + } + + VoteAccount struct { + ActivatedStake int64 `json:"activatedStake"` + Commission int `json:"commission"` + EpochCredits [][]int `json:"epochCredits"` + EpochVoteAccount bool `json:"epochVoteAccount"` + LastVote int `json:"lastVote"` + NodePubkey string `json:"nodePubkey"` + RootSlot int `json:"rootSlot"` + VotePubkey string `json:"votePubkey"` + } + + VoteAccounts struct { + Current []VoteAccount `json:"current"` + Delinquent []VoteAccount `json:"delinquent"` + } + + blockProductionRange struct { + FirstSlot int64 `json:"firstSlot"` + LastSlot *int64 `json:"lastSlot,omitempty"` + } + + blockProductionResult struct { + Value struct { + ByIdentity map[string][]int64 `json:"byIdentity"` + Range blockProductionRange `json:"range"` + } `json:"value"` + } + + BlockProductionPerHost struct { + LeaderSlots int64 + BlocksProduced int64 + } + + BlockProduction struct { + FirstSlot int64 + LastSlot int64 + Hosts map[string]BlockProductionPerHost + } +) + +func (r response[T]) getError() rpcError { + return r.Error +} + +type HasRPCError interface { + getError() rpcError +} diff --git a/pkg/rpc/slot.go b/pkg/rpc/slot.go deleted file mode 100644 index 08d2fcc..0000000 --- a/pkg/rpc/slot.go +++ /dev/null @@ -1,30 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "fmt" - - "k8s.io/klog/v2" -) - -type getSlotResponse struct { - Result int64 `json:"result"` -} - -// https://solana.com/docs/rpc/http/getslot -func (c *Client) GetSlot(ctx context.Context) (int64, error) { - body, err := c.rpcRequest(ctx, formatRPCRequest("getSlot", []interface{}{})) - if err != nil { - return 0, fmt.Errorf("RPC call failed: %w", err) - } - - klog.V(2).Infof("getSlot response: %v", string(body)) - - var resp getSlotResponse - if err = json.Unmarshal(body, &resp); err != nil { - return 0, fmt.Errorf("failed to decode response body: %w", err) - } - - return resp.Result, nil -} diff --git a/pkg/rpc/validators.go b/pkg/rpc/validators.go deleted file mode 100644 index 75aab00..0000000 --- a/pkg/rpc/validators.go +++ /dev/null @@ -1,52 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "fmt" - "k8s.io/klog/v2" -) - -type ( - VoteAccount struct { - ActivatedStake int64 `json:"activatedStake"` - Commission int `json:"commission"` - EpochCredits [][]int `json:"epochCredits"` - EpochVoteAccount bool `json:"epochVoteAccount"` - LastVote int `json:"lastVote"` - NodePubkey string `json:"nodePubkey"` - RootSlot int `json:"rootSlot"` - VotePubkey string `json:"votePubkey"` - } - - VoteAccounts struct { - Current []VoteAccount `json:"current"` - Delinquent []VoteAccount `json:"delinquent"` - } - - GetVoteAccountsResponse struct { - Result VoteAccounts `json:"result"` - Error rpcError1 `json:"error"` - } -) - -// https://docs.solana.com/developing/clients/jsonrpc-api#getvoteaccounts -func (c *Client) GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) { - body, err := c.rpcRequest(ctx, formatRPCRequest("getVoteAccounts", params)) - if err != nil { - return nil, fmt.Errorf("RPC call failed: %w", err) - } - - klog.V(3).Infof("getVoteAccounts response: %v", string(body)) - - var resp GetVoteAccountsResponse - if err = json.Unmarshal(body, &resp); err != nil { - return nil, 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 &resp.Result, nil -} diff --git a/pkg/rpc/version.go b/pkg/rpc/version.go deleted file mode 100644 index 048c233..0000000 --- a/pkg/rpc/version.go +++ /dev/null @@ -1,43 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "fmt" - - "k8s.io/klog/v2" -) - -type ( - GetVersionResponse struct { - Result struct { - Version string `json:"solana-core"` - } `json:"result"` - Error rpcError1 `json:"error"` - } -) - -func (c *Client) GetVersion(ctx context.Context) (string, error) { - body, err := c.rpcRequest(ctx, formatRPCRequest("getVersion", []interface{}{})) - - if body == nil { - return "", fmt.Errorf("RPC call failed: Body empty") - } - - if err != nil { - 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 "", fmt.Errorf("failed to decode response body: %w", err) - } - - if resp.Error.Code != 0 { - return "", fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message) - } - - return resp.Result.Version, nil -}