From f5d604d4f13279152fbb46638c37f3b356420eaf Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Thu, 13 Jun 2024 12:03:49 +0200 Subject: [PATCH 1/8] added dynamic rpc client --- cmd/solana_exporter/exporter_test.go | 9 +- cmd/solana_exporter/utils_test.go | 119 +++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go index 0dd0122..7cb19c4 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/exporter_test.go @@ -113,11 +113,6 @@ func TestSolanaCollector_WatchSlots(t *testing.T) { ) } - hosts := []string{ - "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", - "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", - "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", - } metrics := map[string]*prometheus.CounterVec{ "solana_leader_slots_total": leaderSlotsTotal, "solana_leader_slots_by_epoch": leaderSlotsByEpoch, @@ -129,8 +124,8 @@ func TestSolanaCollector_WatchSlots(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) + for _, testValidator := range testValidators { + testBlockProductionMetric(t, metric, testValidator.identity, status) } }) } diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index c84870e..0b3b670 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -4,7 +4,9 @@ import ( "context" "github.com/certusone/solana_exporter/pkg/rpc" "github.com/prometheus/client_golang/prometheus" + "math/rand" "regexp" + "time" ) type ( @@ -13,6 +15,15 @@ type ( ) var ( + testValidators = []struct { + identity string + vote string + }{ + {"B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"}, + {"C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"}, + {"4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", "xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"}, + } + n = len(testValidators) staticEpochInfo = rpc.EpochInfo{ AbsoluteSlot: 166598, BlockHeight: 166500, @@ -136,3 +147,111 @@ func extractName(desc *prometheus.Desc) string { return name } + +type ( + slotInfo struct { + leader string + blockProduced bool + votes []string + } + dynamicRPCClient struct { + slot int + blockHeight int + epoch int + epochSize int + transactionCount int + version string + slotsInfo map[int]slotInfo + leaderIndex int + } +) + +func (c *dynamicRPCClient) run() { + ticker := time.NewTicker(100 * time.Millisecond) + for { + <-ticker.C + + c.newSlot() + } +} + +func (c *dynamicRPCClient) newSlot() { + c.slot++ + + // leader changes every 4 slots + if c.slot%4 == 0 { + c.leaderIndex = (c.leaderIndex + 1) % n + } + + if c.slot%c.epochSize == 0 { + c.epoch++ + } + + // assume 90% chance of block produced: + blockProduced := rand.Intn(100) > 90 + if blockProduced { + c.blockHeight++ + // only add some transactions if a block was produced + c.transactionCount += rand.Intn(10) + } + + // add slot info: + c.slotsInfo[c.slot] = slotInfo{ + leader: testValidators[c.leaderIndex].identity, + blockProduced: blockProduced, + // assume the other 2 validators voted: + votes: []string{ + testValidators[(c.leaderIndex+1)%n].identity, + testValidators[(c.leaderIndex+2)%n].identity, + }, + } +} + +//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) GetBlockProduction( + ctx context.Context, + firstSlot *int64, + lastSlot *int64, +) (rpc.BlockProduction, error) { + hostProduction := map[string]rpc.BlockProductionPerHost{ + testValidators[0].identity: {0, 0}, + testValidators[1].identity: {0, 0}, + testValidators[2].identity: {0, 0}, + } + for i := *firstSlot; i <= *lastSlot; i++ { + slotInfo := c.slotsInfo[int(i)] + hp := hostProduction[slotInfo.leader] + hp.LeaderSlots++ + if slotInfo.blockProduced { + hp.BlocksProduced++ + } + hostProduction[slotInfo.leader] = hp + } + return rpc.BlockProduction{ + FirstSlot: *firstSlot, + LastSlot: *lastSlot, + Hosts: hostProduction, + }, nil +} From b742be47effd8629b8e4bd83d57e5da5c013ca04 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Fri, 14 Jun 2024 00:13:53 +0200 Subject: [PATCH 2/8] added dynamic unit tests --- cmd/solana_exporter/dynamic_test.go | 212 +++++++++++ cmd/solana_exporter/exporter.go | 8 +- cmd/solana_exporter/slots.go | 2 +- .../{exporter_test.go => static_test.go} | 103 ++--- cmd/solana_exporter/utils_test.go | 351 ++++++++++++------ pkg/rpc/client.go | 2 +- pkg/rpc/version.go | 12 +- 7 files changed, 521 insertions(+), 169 deletions(-) create mode 100644 cmd/solana_exporter/dynamic_test.go rename cmd/solana_exporter/{exporter_test.go => static_test.go} (52%) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go new file mode 100644 index 0000000..5204a3c --- /dev/null +++ b/cmd/solana_exporter/dynamic_test.go @@ -0,0 +1,212 @@ +package main + +import ( + "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) { + // this test passes, however, it seems to cause the static tests to fail (after this test runs, + // the static tests fail to set their correct values to the prometheus metrics). So, putting this + // here while I debug + if testing.Short() { + t.Skip() + } + + // create clients: + client := newDynamicRPCClient() + collector := createSolanaCollector( + client, + 300*time.Millisecond, + ) + prometheus.NewPedanticRegistry().MustRegister(collector) + + // start client/collector and wait a bit: + go client.Run() + time.Sleep(1 * time.Second) + go collector.WatchSlots() + time.Sleep(1 * 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(1 * time.Second) + final := getSlotMetricValues() + + // 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, + ) + + // make current final the new initial (for next iteration) + initial = final + } + + assert.True(t, epochChanged) +} diff --git a/cmd/solana_exporter/exporter.go b/cmd/solana_exporter/exporter.go index 5884b89..88fd3cb 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) } } diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 448db6c..eda74ff 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -89,7 +89,7 @@ func (c *solanaCollector) WatchSlots() { if err != nil { klog.Error(err) } - ticker := time.NewTicker(slotPacerSchedule) + ticker := time.NewTicker(c.slotPace) for { <-ticker.C diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/static_test.go similarity index 52% rename from cmd/solana_exporter/exporter_test.go rename to cmd/solana_exporter/static_test.go index 7cb19c4..72adcdc 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/static_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" @@ -10,71 +9,83 @@ import ( "time" ) -var staticCollector = createSolanaCollector(&staticRPCClient{}) +func TestSolanaCollector_Collect_Static(t *testing.T) { + collector := createSolanaCollector( + &staticRPCClient{}, + slotPacerSchedule, + ) + prometheus.NewPedanticRegistry().MustRegister(collector) -func TestSolanaCollector_Collect(t *testing.T) { - prometheus.NewPedanticRegistry().MustRegister(staticCollector) - - testCases := map[string]string{ - "solana_active_validators": ` + 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 `, - "solana_validator_activated_stake": ` + }, + { + 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="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 49 -solana_validator_activated_stake{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 42 -solana_validator_activated_stake{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 43 +solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49 +solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42 +solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43 `, - "solana_validator_last_vote": ` + }, + { + 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="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 92 -solana_validator_last_vote{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 147 -solana_validator_last_vote{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 148 +solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92 +solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147 +solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148 `, - "solana_validator_root_slot": ` + }, + { + 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="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 3 -solana_validator_root_slot{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 18 -solana_validator_root_slot{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 19 +solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3 +solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18 +solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19 `, - "solana_validator_delinquent": ` + }, + { + Name: "solana_validator_delinquent", + ExpectedResponse: ` # 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_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1 +solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 +solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0 `, - "solana_node_version": ` + }, + { + 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 `, + }, } - 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) - } - }, - ) - } + runCollectionTests(t, collector, testCases) } -func TestSolanaCollector_WatchSlots(t *testing.T) { - go staticCollector.WatchSlots() +func TestSolanaCollector_WatchSlots_Static(t *testing.T) { + collector := createSolanaCollector( + &staticRPCClient{}, + 100*time.Millisecond, + ) + prometheus.NewPedanticRegistry().MustRegister(collector) + go collector.WatchSlots() time.Sleep(1 * time.Second) tests := []struct { @@ -124,8 +135,8 @@ func TestSolanaCollector_WatchSlots(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 _, testValidator := range testValidators { - testBlockProductionMetric(t, metric, testValidator.identity, status) + for _, identity := range identities { + testBlockProductionMetric(t, metric, identity, status) } }) } @@ -154,5 +165,11 @@ func testBlockProductionMetric( labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch)) } // now we can do the assertion: - assert.Equal(t, expectedValue, testutil.ToFloat64(metric.WithLabelValues(labels...))) + 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 0b3b670..d47249d 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -1,29 +1,55 @@ 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" "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 ( - testValidators = []struct { - identity string - vote string - }{ - {"B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"}, - {"C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"}, - {"4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", "xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"}, + identities = []string{ + "aaa", + "bbb", + "ccc", + } + identityVotes = map[string]string{ + "aaa": "AAA", + "bbb": "BBB", + "ccc": "CCC", } - n = len(testValidators) + nv = len(identities) staticEpochInfo = rpc.EpochInfo{ AbsoluteSlot: 166598, BlockHeight: 166500, @@ -36,44 +62,21 @@ var ( FirstSlot: 1000, LastSlot: 2000, Hosts: map[string]rpc.BlockProductionPerHost{ - "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { + "bbb": { LeaderSlots: 400, BlocksProduced: 360, }, - "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { + "ccc": { LeaderSlots: 300, BlocksProduced: 296, }, - "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae": { + "aaa": { LeaderSlots: 300, BlocksProduced: 0, }, }, } -) - -//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, @@ -84,9 +87,9 @@ func (c *staticRPCClient) GetVoteAccounts( }, EpochVoteAccount: true, LastVote: 147, - NodePubkey: "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", + NodePubkey: "bbb", RootSlot: 18, - VotePubkey: "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw", + VotePubkey: "BBB", }, { ActivatedStake: 43, @@ -97,9 +100,9 @@ func (c *staticRPCClient) GetVoteAccounts( }, EpochVoteAccount: true, LastVote: 148, - NodePubkey: "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", + NodePubkey: "ccc", RootSlot: 19, - VotePubkey: "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw", + VotePubkey: "CCC", }, }, Delinquent: []rpc.VoteAccount{ @@ -112,13 +115,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 @@ -130,103 +160,151 @@ func (c *staticRPCClient) GetBlockProduction( return staticBlockProduction, nil } -// 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 - if len(nameMatch) > 1 { - name = nameMatch[1] - } - - return name -} +/* +===== DYNAMIC CLIENT =====: +*/ -type ( - slotInfo struct { - leader string - blockProduced bool - votes []string +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, + } } - dynamicRPCClient struct { - slot int - blockHeight int - epoch int - epochSize int - transactionCount int - version string - slotsInfo map[int]slotInfo - leaderIndex int + 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() { - ticker := time.NewTicker(100 * time.Millisecond) +func (c *dynamicRPCClient) Run() { for { - <-ticker.C - 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++ + c.Slot++ // leader changes every 4 slots - if c.slot%4 == 0 { - c.leaderIndex = (c.leaderIndex + 1) % n + if c.Slot%4 == 0 { + c.LeaderIndex = (c.LeaderIndex + 1) % nv } - if c.slot%c.epochSize == 0 { - c.epoch++ + if c.Slot%c.EpochSize == 0 { + c.Epoch++ } // assume 90% chance of block produced: - blockProduced := rand.Intn(100) > 90 + blockProduced := rand.Intn(100) <= 90 + // add slot info: + c.SlotInfos[c.Slot] = slotInfo{ + leader: identities[c.LeaderIndex], + blockProduced: blockProduced, + } + if blockProduced { - c.blockHeight++ + c.BlockHeight++ // only add some transactions if a block was produced - c.transactionCount += rand.Intn(10) + 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 + } } +} - // add slot info: - c.slotsInfo[c.slot] = slotInfo{ - leader: testValidators[c.leaderIndex].identity, - blockProduced: blockProduced, - // assume the other 2 validators voted: - votes: []string{ - testValidators[(c.leaderIndex+1)%n].identity, - testValidators[(c.leaderIndex+2)%n].identity, - }, - } +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), + 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 + 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) GetVersion(ctx context.Context) (*string, error) { - return &c.version, nil +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 @@ -235,19 +313,18 @@ func (c *dynamicRPCClient) GetBlockProduction( firstSlot *int64, lastSlot *int64, ) (rpc.BlockProduction, error) { - hostProduction := map[string]rpc.BlockProductionPerHost{ - testValidators[0].identity: {0, 0}, - testValidators[1].identity: {0, 0}, - testValidators[2].identity: {0, 0}, + hostProduction := make(map[string]rpc.BlockProductionPerHost) + for _, identity := range identities { + hostProduction[identity] = rpc.BlockProductionPerHost{LeaderSlots: 0, BlocksProduced: 0} } for i := *firstSlot; i <= *lastSlot; i++ { - slotInfo := c.slotsInfo[int(i)] - hp := hostProduction[slotInfo.leader] + info := c.SlotInfos[int(i)] + hp := hostProduction[info.leader] hp.LeaderSlots++ - if slotInfo.blockProduced { + if info.blockProduced { hp.BlocksProduced++ } - hostProduction[slotInfo.leader] = hp + hostProduction[info.leader] = hp } return rpc.BlockProduction{ FirstSlot: *firstSlot, @@ -255,3 +332,47 @@ func (c *dynamicRPCClient) GetBlockProduction( 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 + if len(nameMatch) > 1 { + name = nameMatch[1] + } + + 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) { + if err := testutil.CollectAndCompare( + collector, + bytes.NewBufferString(test.ExpectedResponse), + test.Name, + ); err != nil { + t.Errorf("unexpected collecting result for %s: \n%s", test.Name, err) + } + }, + ) + } +} 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 } From 14cbbde7e4dff94c12b2113cf1c62107a19e7714 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Fri, 14 Jun 2024 00:47:47 +0200 Subject: [PATCH 3/8] fixed dynamic/static test interference --- cmd/solana_exporter/dynamic_test.go | 27 ++++--- cmd/solana_exporter/exporter.go | 2 +- cmd/solana_exporter/slots.go | 115 +++++++++++++++------------- cmd/solana_exporter/static_test.go | 11 ++- cmd/solana_exporter/utils_test.go | 16 ++-- 5 files changed, 96 insertions(+), 75 deletions(-) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go index 5204a3c..b3a785f 100644 --- a/cmd/solana_exporter/dynamic_test.go +++ b/cmd/solana_exporter/dynamic_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" @@ -148,12 +149,9 @@ func getSlotMetricValues() slotMetricValues { } func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { - // this test passes, however, it seems to cause the static tests to fail (after this test runs, - // the static tests fail to set their correct values to the prometheus metrics). So, putting this - // here while I debug - if testing.Short() { - t.Skip() - } + // reset metrics before running tests: + leaderSlotsTotal.Reset() + leaderSlotsByEpoch.Reset() // create clients: client := newDynamicRPCClient() @@ -165,9 +163,10 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { // start client/collector and wait a bit: go client.Run() - time.Sleep(1 * time.Second) - go collector.WatchSlots() - time.Sleep(1 * time.Second) + time.Sleep(time.Second) + ctx, cancel := context.WithCancel(context.Background()) + go collector.WatchSlots(ctx) + time.Sleep(time.Second) initial := getSlotMetricValues() @@ -175,7 +174,7 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { var epochChanged bool for i := 0; i < 5; i++ { // wait a bit then get new metrics - time.Sleep(1 * time.Second) + time.Sleep(time.Second) final := getSlotMetricValues() // make sure that things have increased @@ -203,10 +202,16 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { initial.EpochNumber, final.EpochNumber, ) + if final.EpochNumber > initial.EpochNumber { + epochChanged = true + } // make current final the new initial (for next iteration) initial = final } - assert.True(t, epochChanged) + assert.Truef(t, epochChanged, "Epoch has not changed!") + // cancel and wait for cancellation: + cancel() + time.Sleep(time.Second) } diff --git a/cmd/solana_exporter/exporter.go b/cmd/solana_exporter/exporter.go index 88fd3cb..62a425f 100644 --- a/cmd/solana_exporter/exporter.go +++ b/cmd/solana_exporter/exporter.go @@ -144,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/slots.go b/cmd/solana_exporter/slots.go index eda74ff..21ef135 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) } @@ -92,41 +92,75 @@ func (c *solanaCollector) WatchSlots() { 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.Infof("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.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 index 72adcdc..da487c4 100644 --- a/cmd/solana_exporter/static_test.go +++ b/cmd/solana_exporter/static_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" @@ -80,12 +81,17 @@ solana_node_version{version="1.16.7"} 1 } 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) - go collector.WatchSlots() + ctx, cancel := context.WithCancel(context.Background()) + go collector.WatchSlots(ctx) time.Sleep(1 * time.Second) tests := []struct { @@ -142,6 +148,9 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) { } }) } + // cancel and wait for cancellation: + cancel() + time.Sleep(time.Second) } func testBlockProductionMetric( diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index d47249d..cf29d93 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -59,20 +59,20 @@ var ( TransactionCount: 22661093, } staticBlockProduction = rpc.BlockProduction{ - FirstSlot: 1000, - LastSlot: 2000, + FirstSlot: 100000000, + LastSlot: 200000000, Hosts: map[string]rpc.BlockProductionPerHost{ "bbb": { - LeaderSlots: 400, - BlocksProduced: 360, + LeaderSlots: 40000000, + BlocksProduced: 36000000, }, "ccc": { - LeaderSlots: 300, - BlocksProduced: 296, + LeaderSlots: 30000000, + BlocksProduced: 29600000, }, "aaa": { - LeaderSlots: 300, - BlocksProduced: 0, + LeaderSlots: 30000000, + BlocksProduced: 10000000, }, }, } From e1edb64d0931cf9adbe5e481f6487b52d64ff490 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Fri, 14 Jun 2024 00:53:19 +0200 Subject: [PATCH 4/8] added context to dynamicRPCClient.Run() --- cmd/solana_exporter/dynamic_test.go | 12 ++++++++---- cmd/solana_exporter/utils_test.go | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go index b3a785f..41522f1 100644 --- a/cmd/solana_exporter/dynamic_test.go +++ b/cmd/solana_exporter/dynamic_test.go @@ -162,10 +162,11 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { prometheus.NewPedanticRegistry().MustRegister(collector) // start client/collector and wait a bit: - go client.Run() + runCtx, runCancel := context.WithCancel(context.Background()) + go client.Run(runCtx) time.Sleep(time.Second) - ctx, cancel := context.WithCancel(context.Background()) - go collector.WatchSlots(ctx) + slotsCtx, slotsCancel := context.WithCancel(context.Background()) + go collector.WatchSlots(slotsCtx) time.Sleep(time.Second) initial := getSlotMetricValues() @@ -210,8 +211,11 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { initial = final } + // epoch should have changed somewhere assert.Truef(t, epochChanged, "Epoch has not changed!") + // cancel and wait for cancellation: - cancel() + slotsCancel() + runCancel() time.Sleep(time.Second) } diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index cf29d93..be0fdfc 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -188,13 +188,19 @@ func newDynamicRPCClient() *dynamicRPCClient { } } -func (c *dynamicRPCClient) Run() { +func (c *dynamicRPCClient) Run(ctx context.Context) { for { - 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)) + 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)) + } } } From 206aa75ecaba1ded9ec14a8059f0ef31237050ae Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Fri, 14 Jun 2024 09:56:23 +0200 Subject: [PATCH 5/8] split out correct metric changes --- cmd/solana_exporter/dynamic_test.go | 71 +++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go index 41522f1..95e69f5 100644 --- a/cmd/solana_exporter/dynamic_test.go +++ b/cmd/solana_exporter/dynamic_test.go @@ -178,31 +178,36 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { time.Sleep(time.Second) final := getSlotMetricValues() - // make sure that things have increased - assert.Greaterf( + // 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, - final.SlotHeight, - initial.SlotHeight, - "Slot has not increased! (%v -> %v)", - initial.SlotHeight, - final.SlotHeight, + int(final.SlotHeight), + client.Slot, + "Exporter slot (%v) ahead of client slot (%v)!", + int(final.SlotHeight), + client.Slot, ) - assert.Greaterf( + assert.LessOrEqualf( t, - final.TotalTransactions, - initial.TotalTransactions, - "Total transactions have not increased! (%v -> %v)", - initial.TotalTransactions, - final.TotalTransactions, + int(final.TotalTransactions), + client.TransactionCount, + "Exporter transaction count (%v) ahead of client transaction count (%v)!", + int(final.TotalTransactions), + client.TransactionCount, ) - assert.GreaterOrEqualf( + assert.LessOrEqualf( t, - final.EpochNumber, - initial.EpochNumber, - "Epoch number has decreased! (%v -> %v)", - initial.EpochNumber, - final.EpochNumber, + 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 } @@ -219,3 +224,31 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { runCancel() time.Sleep(time.Second) } + +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, + ) +} From 1bf607fa102f14c4ac44f338e92137ddc02a69d0 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Wed, 12 Jun 2024 22:59:37 +0200 Subject: [PATCH 6/8] updated go version -> 1.22 --- Dockerfile | 2 +- go.mod | 21 ++++++++++++++++----- go.sum | 21 ++++++++++++++++++++- 3 files changed, 37 insertions(+), 7 deletions(-) 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/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= From fb5ceb82d704b1be326262feaff690b8e2ef6428 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Fri, 14 Jun 2024 21:31:45 +0200 Subject: [PATCH 7/8] reduced indentation (for jeff) --- cmd/solana_exporter/dynamic_test.go | 10 ++---- cmd/solana_exporter/static_test.go | 36 ++++++-------------- cmd/solana_exporter/utils_test.go | 51 +++++++---------------------- 3 files changed, 23 insertions(+), 74 deletions(-) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go index 95e69f5..0bcdc09 100644 --- a/cmd/solana_exporter/dynamic_test.go +++ b/cmd/solana_exporter/dynamic_test.go @@ -11,10 +11,7 @@ import ( func TestSolanaCollector_Collect_Dynamic(t *testing.T) { client := newDynamicRPCClient() - collector := createSolanaCollector( - client, - slotPacerSchedule, - ) + collector := createSolanaCollector(client, slotPacerSchedule) prometheus.NewPedanticRegistry().MustRegister(collector) // start off by testing initial state: @@ -155,10 +152,7 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { // create clients: client := newDynamicRPCClient() - collector := createSolanaCollector( - client, - 300*time.Millisecond, - ) + collector := createSolanaCollector(client, 300*time.Millisecond) prometheus.NewPedanticRegistry().MustRegister(collector) // start client/collector and wait a bit: diff --git a/cmd/solana_exporter/static_test.go b/cmd/solana_exporter/static_test.go index da487c4..09f0906 100644 --- a/cmd/solana_exporter/static_test.go +++ b/cmd/solana_exporter/static_test.go @@ -94,40 +94,24 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) { 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(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex), - metric: epochFirstSlot, - }, - { - expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex + staticEpochInfo.SlotsInEpoch), - metric: epochLastSlot, - }, + {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)) - }, - ) + t.Run(name, func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric)) + }) } metrics := map[string]*prometheus.CounterVec{ diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index be0fdfc..03d5486 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -6,6 +6,7 @@ import ( "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" @@ -39,16 +40,8 @@ type ( ) var ( - identities = []string{ - "aaa", - "bbb", - "ccc", - } - identityVotes = map[string]string{ - "aaa": "AAA", - "bbb": "BBB", - "ccc": "CCC", - } + identities = []string{"aaa", "bbb", "ccc"} + identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"} nv = len(identities) staticEpochInfo = rpc.EpochInfo{ AbsoluteSlot: 166598, @@ -62,18 +55,9 @@ var ( FirstSlot: 100000000, LastSlot: 200000000, Hosts: map[string]rpc.BlockProductionPerHost{ - "bbb": { - LeaderSlots: 40000000, - BlocksProduced: 36000000, - }, - "ccc": { - LeaderSlots: 30000000, - BlocksProduced: 29600000, - }, - "aaa": { - LeaderSlots: 30000000, - BlocksProduced: 10000000, - }, + "bbb": {LeaderSlots: 40000000, BlocksProduced: 36000000}, + "ccc": {LeaderSlots: 30000000, BlocksProduced: 29600000}, + "aaa": {LeaderSlots: 30000000, BlocksProduced: 10000000}, }, } staticVoteAccounts = rpc.VoteAccounts{ @@ -307,10 +291,7 @@ func (c *dynamicRPCClient) GetVoteAccounts( currentVoteAccounts = append(currentVoteAccounts, voteAccount) } } - return &rpc.VoteAccounts{ - Current: currentVoteAccounts, - Delinquent: delinquentVoteAccounts, - }, nil + return &rpc.VoteAccounts{Current: currentVoteAccounts, Delinquent: delinquentVoteAccounts}, nil } //goland:noinspection GoUnusedParameter @@ -347,10 +328,8 @@ func (c *dynamicRPCClient) GetBlockProduction( 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 @@ -368,17 +347,9 @@ type collectionTest struct { func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases []collectionTest) { for _, test := range testCases { - t.Run( - test.Name, - func(t *testing.T) { - if err := testutil.CollectAndCompare( - collector, - bytes.NewBufferString(test.ExpectedResponse), - test.Name, - ); err != nil { - t.Errorf("unexpected collecting result for %s: \n%s", test.Name, err) - } - }, - ) + 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) + }) } } From 214b7370811a5cb8bcef83b38dee9a521c10585e Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Sat, 15 Jun 2024 09:57:41 +0200 Subject: [PATCH 8/8] implemented jeffs suggestions --- cmd/solana_exporter/dynamic_test.go | 8 +++----- cmd/solana_exporter/slots.go | 4 ++-- cmd/solana_exporter/static_test.go | 4 +--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go index 0bcdc09..c0d88b5 100644 --- a/cmd/solana_exporter/dynamic_test.go +++ b/cmd/solana_exporter/dynamic_test.go @@ -157,9 +157,12 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { // 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) @@ -212,11 +215,6 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) { // epoch should have changed somewhere assert.Truef(t, epochChanged, "Epoch has not changed!") - - // cancel and wait for cancellation: - slotsCancel() - runCancel() - time.Sleep(time.Second) } func assertSlotMetricsChangeCorrectly(t *testing.T, initial slotMetricValues, final slotMetricValues) { diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 21ef135..fdf4d58 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -104,14 +104,14 @@ func (c *solanaCollector) WatchSlots(ctx context.Context) { 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) + klog.Warningf("failed to fetch epoch info, retrying: %v", err) cancel() continue } cancel() if watermark == info.AbsoluteSlot { - klog.Infof("slot has not advanced at %d, skipping", info.AbsoluteSlot) + klog.V(2).Infof("slot has not advanced at %d, skipping", info.AbsoluteSlot) continue } diff --git a/cmd/solana_exporter/static_test.go b/cmd/solana_exporter/static_test.go index 09f0906..e9dd6c8 100644 --- a/cmd/solana_exporter/static_test.go +++ b/cmd/solana_exporter/static_test.go @@ -91,6 +91,7 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) { ) prometheus.NewPedanticRegistry().MustRegister(collector) ctx, cancel := context.WithCancel(context.Background()) + defer cancel() go collector.WatchSlots(ctx) time.Sleep(1 * time.Second) @@ -132,9 +133,6 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) { } }) } - // cancel and wait for cancellation: - cancel() - time.Sleep(time.Second) } func testBlockProductionMetric(