From 815e8c14f9de6551708ffa079e117ce86353a153 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Wed, 12 Jun 2024 11:21:16 +0200 Subject: [PATCH 1/6] added static tests for exporter.go --- cmd/solana_exporter/exporter.go | 16 ++-- cmd/solana_exporter/exporter_test.go | 132 +++++++++++++++++++++++++++ cmd/solana_exporter/utils_test.go | 113 +++++++++++++++++++++++ go.mod | 2 + go.sum | 7 ++ pkg/rpc/blockproduction.go | 8 +- pkg/rpc/client.go | 6 +- pkg/rpc/epochinfo.go | 2 +- pkg/rpc/validators.go | 16 ++-- pkg/rpc/version.go | 2 +- 10 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 cmd/solana_exporter/exporter_test.go create mode 100644 cmd/solana_exporter/utils_test.go diff --git a/cmd/solana_exporter/exporter.go b/cmd/solana_exporter/exporter.go index ec6aa5b..5884b89 100644 --- a/cmd/solana_exporter/exporter.go +++ b/cmd/solana_exporter/exporter.go @@ -72,15 +72,19 @@ func NewSolanaCollector(rpcAddr string) *solanaCollector { func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.totalValidatorsDesc ch <- c.solanaVersion + ch <- c.validatorActivatedStake + ch <- c.validatorLastVote + ch <- c.validatorRootSlot + ch <- c.validatorDelinquent } -func (c *solanaCollector) mustEmitMetrics(ch chan<- prometheus.Metric, response *rpc.GetVoteAccountsResponse) { +func (c *solanaCollector) mustEmitMetrics(ch chan<- prometheus.Metric, response *rpc.VoteAccounts) { ch <- prometheus.MustNewConstMetric(c.totalValidatorsDesc, prometheus.GaugeValue, - float64(len(response.Result.Delinquent)), "delinquent") + float64(len(response.Delinquent)), "delinquent") ch <- prometheus.MustNewConstMetric(c.totalValidatorsDesc, prometheus.GaugeValue, - float64(len(response.Result.Current)), "current") + float64(len(response.Current)), "current") - for _, account := range append(response.Result.Current, response.Result.Delinquent...) { + for _, account := range append(response.Current, response.Delinquent...) { ch <- prometheus.MustNewConstMetric(c.validatorActivatedStake, prometheus.GaugeValue, float64(account.ActivatedStake), account.VotePubkey, account.NodePubkey) ch <- prometheus.MustNewConstMetric(c.validatorLastVote, prometheus.GaugeValue, @@ -88,11 +92,11 @@ func (c *solanaCollector) mustEmitMetrics(ch chan<- prometheus.Metric, response ch <- prometheus.MustNewConstMetric(c.validatorRootSlot, prometheus.GaugeValue, float64(account.RootSlot), account.VotePubkey, account.NodePubkey) } - for _, account := range response.Result.Current { + for _, account := range response.Current { ch <- prometheus.MustNewConstMetric(c.validatorDelinquent, prometheus.GaugeValue, 0, account.VotePubkey, account.NodePubkey) } - for _, account := range response.Result.Delinquent { + for _, account := range response.Delinquent { ch <- prometheus.MustNewConstMetric(c.validatorDelinquent, prometheus.GaugeValue, 1, account.VotePubkey, account.NodePubkey) } diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go new file mode 100644 index 0000000..c9d44fb --- /dev/null +++ b/cmd/solana_exporter/exporter_test.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "testing" +) + +var staticCollector = createSolanaCollector(&staticRPCClient{}) + +func TestSolanaCollector_Collect(t *testing.T) { + prometheus.NewPedanticRegistry().MustRegister(staticCollector) + + t.Run( + "TestTotalValidators", + func(t *testing.T) { + expectedTotalValidators := ` +# 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 +` + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedTotalValidators), + "solana_active_validators", + ); err != nil { + t.Errorf("unexpected collecting result for total validators:\n%s", err) + } + }, + ) + + t.Run( + "TestActivatedStake", + func(t *testing.T) { + expectedActivatedStake := ` +# 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 +` + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedActivatedStake), + "solana_validator_activated_stake", + ); err != nil { + t.Errorf("unexpected collecting result for activated stake:\n%s", err) + } + }, + ) + + t.Run( + "TestLastVote", + func(t *testing.T) { + expectedLastVote := ` +# 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 +` + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedLastVote), + "solana_validator_last_vote", + ); err != nil { + t.Errorf("unexpected collecting result for last vote:\n%s", err) + } + }, + ) + + t.Run( + "TestRootSlot", + func(t *testing.T) { + expectedRootSlot := ` +# 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 +` + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedRootSlot), + "solana_validator_root_slot", + ); err != nil { + t.Errorf("unexpected collecting result for root slot:\n%s", err) + } + }, + ) + + t.Run( + "TestValidatorDelinquent", + func(t *testing.T) { + expectedRootSlot := ` +# 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 +` + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedRootSlot), + "solana_validator_delinquent", + ); err != nil { + t.Errorf("unexpected collecting result for validator delinquent:\n%s", err) + } + }, + ) + + t.Run( + "TestNodeVersion", + func(t *testing.T) { + expectedRootSlot := ` +# HELP solana_node_version Node version of solana +# TYPE solana_node_version gauge +solana_node_version{version="1.16.7"} 1 + +` + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedRootSlot), + "solana_node_version", + ); err != nil { + t.Errorf("unexpected collecting result for node version:\n%s", err) + } + }, + ) +} diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go new file mode 100644 index 0000000..ce03210 --- /dev/null +++ b/cmd/solana_exporter/utils_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "github.com/certusone/solana_exporter/pkg/rpc" +) + +type ( + staticRPCClient struct{} + // TODO: create dynamicRPCClient + according tests! +) + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) { + return &rpc.EpochInfo{ + AbsoluteSlot: 166598, + BlockHeight: 166500, + Epoch: 27, + SlotIndex: 2790, + SlotsInEpoch: 8192, + TransactionCount: 22661093, + }, nil +} + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetSlot(ctx context.Context) (int64, error) { + return 166598, 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{ + Current: []rpc.VoteAccount{ + { + ActivatedStake: 42, + Commission: 0, + EpochCredits: [][]int{ + {1, 64, 0}, + {2, 192, 64}, + }, + EpochVoteAccount: true, + LastVote: 147, + NodePubkey: "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", + RootSlot: 18, + VotePubkey: "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw", + }, + { + ActivatedStake: 43, + Commission: 1, + EpochCredits: [][]int{ + {2, 65, 1}, + {3, 193, 65}, + }, + EpochVoteAccount: true, + LastVote: 148, + NodePubkey: "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", + RootSlot: 19, + VotePubkey: "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw", + }, + }, + Delinquent: []rpc.VoteAccount{ + { + ActivatedStake: 49, + Commission: 2, + EpochCredits: [][]int{ + {10, 594, 6}, + {9, 98, 4}, + }, + EpochVoteAccount: true, + LastVote: 92, + NodePubkey: "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", + RootSlot: 3, + VotePubkey: "xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU", + }, + }, + } + return &voteAccounts, nil +} + +//goland:noinspection GoUnusedParameter +func (c *staticRPCClient) GetBlockProduction( + ctx context.Context, + firstSlot *int64, + lastSlot *int64, +) (rpc.BlockProduction, error) { + return rpc.BlockProduction{ + FirstSlot: 1000, + LastSlot: 2000, + Hosts: map[string]rpc.BlockProductionPerHost{ + "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { + LeaderSlots: 400, + BlocksProduced: 360, + }, + "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { + LeaderSlots: 300, + BlocksProduced: 296, + }, + "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae": { + LeaderSlots: 300, + BlocksProduced: 0, + }, + }, + }, nil +} diff --git a/go.mod b/go.mod index 2cf8bae..66711a2 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,7 @@ go 1.13 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 ) diff --git a/go.sum b/go.sum index 525bb83..c509d19 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r 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/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= 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= @@ -31,8 +32,10 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u 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= @@ -43,6 +46,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb 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/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= @@ -65,6 +69,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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= 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= @@ -84,10 +89,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= 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= 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= diff --git a/pkg/rpc/blockproduction.go b/pkg/rpc/blockproduction.go index d0d69f8..1c1c7bd 100644 --- a/pkg/rpc/blockproduction.go +++ b/pkg/rpc/blockproduction.go @@ -32,7 +32,7 @@ type ( Error rpcError2 `json:"error"` } - blockProductionPerHost struct { + BlockProductionPerHost struct { LeaderSlots int64 BlocksProduced int64 } @@ -40,7 +40,7 @@ type ( BlockProduction struct { FirstSlot int64 LastSlot int64 - Hosts map[string]blockProductionPerHost + Hosts map[string]BlockProductionPerHost } ) @@ -81,10 +81,10 @@ func (c *Client) GetBlockProduction(ctx context.Context, firstSlot *int64, lastS ret.FirstSlot = resp.Result.Value.Range.FirstSlot ret.LastSlot = *resp.Result.Value.Range.LastSlot - ret.Hosts = make(map[string]blockProductionPerHost) + ret.Hosts = make(map[string]BlockProductionPerHost) for id, arr := range resp.Result.Value.ByIdentity { - ret.Hosts[id] = blockProductionPerHost{ + ret.Hosts[id] = BlockProductionPerHost{ LeaderSlots: arr[0], BlocksProduced: arr[1], } diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index 6b25895..ba2ed70 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -15,12 +15,12 @@ type ( rpcAddr string } - rpcError struct { + rpcError1 struct { Message string `json:"message"` Code int64 `json:"id"` } - rpcError2 struct { + rpcError2 struct { // TODO: combine these error types into a single one Message string `json:"message"` Code int64 `json:"code"` } @@ -58,7 +58,7 @@ type Provider interface { // 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, // or an error if the operation fails. - GetVoteAccounts(ctx context.Context, params []interface{}) (*GetVoteAccountsResponse, error) + GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) // GetVersion retrieves the version of the Solana node. // The method takes a context for cancellation. diff --git a/pkg/rpc/epochinfo.go b/pkg/rpc/epochinfo.go index 2589110..de0369c 100644 --- a/pkg/rpc/epochinfo.go +++ b/pkg/rpc/epochinfo.go @@ -25,7 +25,7 @@ type ( GetEpochInfoResponse struct { Result EpochInfo `json:"result"` - Error rpcError `json:"error"` + Error rpcError1 `json:"error"` } ) diff --git a/pkg/rpc/validators.go b/pkg/rpc/validators.go index 2347ad1..75aab00 100644 --- a/pkg/rpc/validators.go +++ b/pkg/rpc/validators.go @@ -19,17 +19,19 @@ type ( VotePubkey string `json:"votePubkey"` } + VoteAccounts struct { + Current []VoteAccount `json:"current"` + Delinquent []VoteAccount `json:"delinquent"` + } + GetVoteAccountsResponse struct { - Result struct { - Current []VoteAccount `json:"current"` - Delinquent []VoteAccount `json:"delinquent"` - } `json:"result"` - Error rpcError `json:"error"` + 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{}) (*GetVoteAccountsResponse, error) { +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) @@ -46,5 +48,5 @@ func (c *Client) GetVoteAccounts(ctx context.Context, params []interface{}) (*Ge return nil, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message) } - return &resp, nil + return &resp.Result, nil } diff --git a/pkg/rpc/version.go b/pkg/rpc/version.go index ed1b6e1..abfe1b9 100644 --- a/pkg/rpc/version.go +++ b/pkg/rpc/version.go @@ -13,7 +13,7 @@ type ( Result struct { Version string `json:"solana-core"` } `json:"result"` - Error rpcError `json:"error"` + Error rpcError1 `json:"error"` } ) From 286cbc53e8db56e2f12409f2b5cd7af7b8b175bd Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Wed, 12 Jun 2024 17:17:01 +0200 Subject: [PATCH 2/6] added watch slots test --- cmd/solana_exporter/exporter_test.go | 43 +++++++++++++++++++++++++++- cmd/solana_exporter/utils_test.go | 14 +++++---- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go index c9d44fb..1c517bb 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/exporter_test.go @@ -4,7 +4,9 @@ import ( "bytes" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" "testing" + "time" ) var staticCollector = createSolanaCollector(&staticRPCClient{}) @@ -118,7 +120,6 @@ solana_validator_delinquent{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTD # HELP solana_node_version Node version of solana # TYPE solana_node_version gauge solana_node_version{version="1.16.7"} 1 - ` if err := testutil.CollectAndCompare( staticCollector, @@ -130,3 +131,43 @@ solana_node_version{version="1.16.7"} 1 }, ) } + +func TestSolanaCollector_WatchSlots(t *testing.T) { + go staticCollector.WatchSlots() + time.Sleep(1 * time.Second) + + tests := map[string]struct { + expectedValue float64 + metric prometheus.Collector + }{ + "ConfirmedSlotHeight": { + expectedValue: float64(staticEpochInfo.AbsoluteSlot), + metric: confirmedSlotHeight, + }, + "TotalTransactions": { + expectedValue: float64(staticEpochInfo.TransactionCount), + metric: totalTransactionsTotal, + }, + "CurrentEpochNumber": { + expectedValue: float64(staticEpochInfo.Epoch), + metric: currentEpochNumber, + }, + "EpochFirstSlot": { + expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex), + metric: epochFirstSlot, + }, + "EpochLastSlot": { + expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex + staticEpochInfo.SlotsInEpoch), + metric: epochLastSlot, + }, + } + + for name, testCase := range tests { + t.Run( + name, + func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric)) + }, + ) + } +} diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index ce03210..af35af9 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -10,21 +10,25 @@ type ( // TODO: create dynamicRPCClient + according tests! ) -//goland:noinspection GoUnusedParameter -func (c *staticRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) { - return &rpc.EpochInfo{ +var ( + staticEpochInfo = rpc.EpochInfo{ AbsoluteSlot: 166598, BlockHeight: 166500, Epoch: 27, SlotIndex: 2790, SlotsInEpoch: 8192, TransactionCount: 22661093, - }, nil + } +) + +//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 166598, nil + return staticEpochInfo.AbsoluteSlot, nil } //goland:noinspection GoUnusedParameter From 7912a035aab81e103598ec3d102a2dde632a0e10 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Wed, 12 Jun 2024 17:52:51 +0200 Subject: [PATCH 3/6] add initial set to WatchSlots() --- cmd/solana_exporter/slots.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 00ea289..b6d39ad 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -74,9 +74,15 @@ func (c *solanaCollector) WatchSlots() { } cancel() + totalTransactionsTotal.Set(float64(info.TransactionCount)) + confirmedSlotHeight.Set(float64(info.AbsoluteSlot)) + // watermark is the last slot number we generated ticks for. Set it to the current offset on startup (we do not backfill slots we missed at startup) watermark := info.AbsoluteSlot currentEpoch, firstSlot, lastSlot := getEpochBounds(info) + currentEpochNumber.Set(float64(currentEpoch)) + epochFirstSlot.Set(float64(firstSlot)) + epochLastSlot.Set(float64(lastSlot)) klog.Infof("Starting at slot %d in epoch %d (%d-%d)", firstSlot, currentEpoch, firstSlot, lastSlot) ticker := time.NewTicker(slotPacerSchedule) From f8610465105edee6d37e50a529ed2f858daf80fc Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Wed, 12 Jun 2024 23:49:16 +0200 Subject: [PATCH 4/6] dried tests + made test name match metric name --- cmd/solana_exporter/exporter_test.go | 135 ++++++++------------------- cmd/solana_exporter/utils_test.go | 20 ++++ 2 files changed, 58 insertions(+), 97 deletions(-) diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go index 1c517bb..b876aca 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/exporter_test.go @@ -14,155 +14,96 @@ var staticCollector = createSolanaCollector(&staticRPCClient{}) func TestSolanaCollector_Collect(t *testing.T) { prometheus.NewPedanticRegistry().MustRegister(staticCollector) - t.Run( - "TestTotalValidators", - func(t *testing.T) { - expectedTotalValidators := ` + testCases := map[string]string{ + "solana_active_validators": ` # HELP solana_active_validators Total number of active validators by state # TYPE solana_active_validators gauge solana_active_validators{state="current"} 2 solana_active_validators{state="delinquent"} 1 -` - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedTotalValidators), - "solana_active_validators", - ); err != nil { - t.Errorf("unexpected collecting result for total validators:\n%s", err) - } - }, - ) - - t.Run( - "TestActivatedStake", - func(t *testing.T) { - expectedActivatedStake := ` +`, + "solana_validator_activated_stake": ` # HELP solana_validator_activated_stake Activated stake per validator # TYPE solana_validator_activated_stake gauge solana_validator_activated_stake{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 49 solana_validator_activated_stake{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 42 solana_validator_activated_stake{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 43 -` - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedActivatedStake), - "solana_validator_activated_stake", - ); err != nil { - t.Errorf("unexpected collecting result for activated stake:\n%s", err) - } - }, - ) - - t.Run( - "TestLastVote", - func(t *testing.T) { - expectedLastVote := ` +`, + "solana_validator_last_vote": ` # HELP solana_validator_last_vote Last voted slot per validator # TYPE solana_validator_last_vote gauge solana_validator_last_vote{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 92 solana_validator_last_vote{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 147 solana_validator_last_vote{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 148 -` - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedLastVote), - "solana_validator_last_vote", - ); err != nil { - t.Errorf("unexpected collecting result for last vote:\n%s", err) - } - }, - ) - - t.Run( - "TestRootSlot", - func(t *testing.T) { - expectedRootSlot := ` +`, + "solana_validator_root_slot": ` # HELP solana_validator_root_slot Root slot per validator # TYPE solana_validator_root_slot gauge solana_validator_root_slot{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 3 solana_validator_root_slot{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 18 solana_validator_root_slot{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 19 -` - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedRootSlot), - "solana_validator_root_slot", - ); err != nil { - t.Errorf("unexpected collecting result for root slot:\n%s", err) - } - }, - ) - - t.Run( - "TestValidatorDelinquent", - func(t *testing.T) { - expectedRootSlot := ` +`, + "solana_validator_delinquent": ` # HELP solana_validator_delinquent Whether a validator is delinquent # TYPE solana_validator_delinquent gauge solana_validator_delinquent{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 1 solana_validator_delinquent{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0 solana_validator_delinquent{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0 -` - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedRootSlot), - "solana_validator_delinquent", - ); err != nil { - t.Errorf("unexpected collecting result for validator delinquent:\n%s", err) - } - }, - ) - - t.Run( - "TestNodeVersion", - func(t *testing.T) { - expectedRootSlot := ` +`, + "solana_node_version": ` # HELP solana_node_version Node version of solana # TYPE solana_node_version gauge solana_node_version{version="1.16.7"} 1 -` - if err := testutil.CollectAndCompare( - staticCollector, - bytes.NewBufferString(expectedRootSlot), - "solana_node_version", - ); err != nil { - t.Errorf("unexpected collecting result for node version:\n%s", err) - } - }, - ) +`, + } + + for testName, expectedValue := range testCases { + t.Run( + testName, + func(t *testing.T) { + if err := testutil.CollectAndCompare( + staticCollector, + bytes.NewBufferString(expectedValue), + testName, + ); err != nil { + t.Errorf("unexpected collecting result for %s: \n%s", testName, err) + } + }, + ) + } } func TestSolanaCollector_WatchSlots(t *testing.T) { go staticCollector.WatchSlots() time.Sleep(1 * time.Second) - tests := map[string]struct { + tests := []struct { expectedValue float64 - metric prometheus.Collector + metric prometheus.Gauge }{ - "ConfirmedSlotHeight": { + { expectedValue: float64(staticEpochInfo.AbsoluteSlot), metric: confirmedSlotHeight, }, - "TotalTransactions": { + { expectedValue: float64(staticEpochInfo.TransactionCount), metric: totalTransactionsTotal, }, - "CurrentEpochNumber": { + { expectedValue: float64(staticEpochInfo.Epoch), metric: currentEpochNumber, }, - "EpochFirstSlot": { + { expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex), metric: epochFirstSlot, }, - "EpochLastSlot": { + { expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex + staticEpochInfo.SlotsInEpoch), metric: epochLastSlot, }, } - for name, testCase := range tests { + for _, testCase := range tests { + name := extractName(testCase.metric.Desc()) t.Run( name, func(t *testing.T) { diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index af35af9..6fe8fa5 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -3,6 +3,8 @@ package main import ( "context" "github.com/certusone/solana_exporter/pkg/rpc" + "github.com/prometheus/client_golang/prometheus" + "regexp" ) type ( @@ -115,3 +117,21 @@ func (c *staticRPCClient) GetBlockProduction( }, }, 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 +} From 28fd5e15ddf1e6aeb2621921ca6991706dd5d958 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Thu, 13 Jun 2024 00:42:24 +0200 Subject: [PATCH 5/6] added block production tests --- cmd/solana_exporter/exporter_test.go | 49 ++++++++++++++++++++++++++++ cmd/solana_exporter/slots.go | 6 ++++ cmd/solana_exporter/utils_test.go | 37 +++++++++++---------- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/exporter_test.go index b876aca..0dd0122 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/exporter_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" @@ -111,4 +112,52 @@ 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, + } + statuses := []string{"valid", "skipped"} + for name, metric := range metrics { + // subtest for each metric: + t.Run(name, func(t *testing.T) { + for _, status := range statuses { + // sub subtest for each status (as each one requires a different calc) + t.Run(status, func(t *testing.T) { + for _, host := range hosts { + testBlockProductionMetric(t, metric, host, status) + } + }) + } + }) + } +} + +func testBlockProductionMetric( + t *testing.T, + metric *prometheus.CounterVec, + host string, + status string, +) { + hostInfo := staticBlockProduction.Hosts[host] + // get expected value depending on status: + var expectedValue float64 + switch status { + case "valid": + expectedValue = float64(hostInfo.BlocksProduced) + case "skipped": + expectedValue = float64(hostInfo.LeaderSlots - hostInfo.BlocksProduced) + } + // get labels (leaderSlotsByEpoch requires an extra one) + labels := []string{status, host} + if metric == leaderSlotsByEpoch { + labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch)) + } + // now we can do the assertion: + assert.Equal(t, expectedValue, testutil.ToFloat64(metric.WithLabelValues(labels...))) } diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index b6d39ad..569aa89 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log" "time" "github.com/certusone/solana_exporter/pkg/rpc" @@ -85,6 +86,10 @@ func (c *solanaCollector) WatchSlots() { epochLastSlot.Set(float64(lastSlot)) klog.Infof("Starting at slot %d in epoch %d (%d-%d)", firstSlot, currentEpoch, firstSlot, lastSlot) + _, err = updateCounters(c.rpcClient, currentEpoch, watermark, &lastSlot) + if err != nil { + klog.Error(err) + } ticker := time.NewTicker(slotPacerSchedule) for { @@ -207,6 +212,7 @@ func updateCounters(c rpc.Provider, epoch, firstSlot int64, lastSlotOpt *int64) } for host, prod := range blockProduction.Hosts { + log.Print(host) valid := float64(prod.BlocksProduced) skipped := float64(prod.LeaderSlots - prod.BlocksProduced) diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index 6fe8fa5..c84870e 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -21,6 +21,24 @@ var ( SlotsInEpoch: 8192, TransactionCount: 22661093, } + staticBlockProduction = rpc.BlockProduction{ + FirstSlot: 1000, + LastSlot: 2000, + Hosts: map[string]rpc.BlockProductionPerHost{ + "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { + LeaderSlots: 400, + BlocksProduced: 360, + }, + "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { + LeaderSlots: 300, + BlocksProduced: 296, + }, + "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae": { + LeaderSlots: 300, + BlocksProduced: 0, + }, + }, + } ) //goland:noinspection GoUnusedParameter @@ -98,24 +116,7 @@ func (c *staticRPCClient) GetBlockProduction( firstSlot *int64, lastSlot *int64, ) (rpc.BlockProduction, error) { - return rpc.BlockProduction{ - FirstSlot: 1000, - LastSlot: 2000, - Hosts: map[string]rpc.BlockProductionPerHost{ - "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { - LeaderSlots: 400, - BlocksProduced: 360, - }, - "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": { - LeaderSlots: 300, - BlocksProduced: 296, - }, - "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae": { - LeaderSlots: 300, - BlocksProduced: 0, - }, - }, - }, nil + return staticBlockProduction, nil } // extractName takes a Prometheus descriptor and returns its name From aad48f64ed70f000d730fcdae0aad0134b658418 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Thu, 13 Jun 2024 00:48:21 +0200 Subject: [PATCH 6/6] removed print statement --- cmd/solana_exporter/slots.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 569aa89..448db6c 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "log" "time" "github.com/certusone/solana_exporter/pkg/rpc" @@ -212,7 +211,6 @@ func updateCounters(c rpc.Provider, epoch, firstSlot int64, lastSlotOpt *int64) } for host, prod := range blockProduction.Hosts { - log.Print(host) valid := float64(prod.BlocksProduced) skipped := float64(prod.LeaderSlots - prod.BlocksProduced)