Skip to content

Commit

Permalink
Merge pull request asymmetric-research#25 from asymmetric-research/pr…
Browse files Browse the repository at this point in the history
…ovider-interface

Static tests
  • Loading branch information
johnstonematt authored Jun 15, 2024
2 parents 91bc434 + aad48f6 commit f1e948f
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 22 deletions.
16 changes: 10 additions & 6 deletions cmd/solana_exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,31 @@ 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,
float64(account.LastVote), account.VotePubkey, account.NodePubkey)
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)
}
Expand Down
163 changes: 163 additions & 0 deletions cmd/solana_exporter/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package main

import (
"bytes"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"testing"
"time"
)

var staticCollector = createSolanaCollector(&staticRPCClient{})

func TestSolanaCollector_Collect(t *testing.T) {
prometheus.NewPedanticRegistry().MustRegister(staticCollector)

testCases := map[string]string{
"solana_active_validators": `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
"solana_validator_activated_stake": `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 49
solana_validator_activated_stake{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 42
solana_validator_activated_stake{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 43
`,
"solana_validator_last_vote": `
# HELP solana_validator_last_vote Last voted slot per validator
# TYPE solana_validator_last_vote gauge
solana_validator_last_vote{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 92
solana_validator_last_vote{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 147
solana_validator_last_vote{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 148
`,
"solana_validator_root_slot": `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 3
solana_validator_root_slot{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 18
solana_validator_root_slot{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 19
`,
"solana_validator_delinquent": `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 1
solana_validator_delinquent{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0
solana_validator_delinquent{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0
`,
"solana_node_version": `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="1.16.7"} 1
`,
}

for testName, expectedValue := range testCases {
t.Run(
testName,
func(t *testing.T) {
if err := testutil.CollectAndCompare(
staticCollector,
bytes.NewBufferString(expectedValue),
testName,
); err != nil {
t.Errorf("unexpected collecting result for %s: \n%s", testName, err)
}
},
)
}
}

func TestSolanaCollector_WatchSlots(t *testing.T) {
go staticCollector.WatchSlots()
time.Sleep(1 * time.Second)

tests := []struct {
expectedValue float64
metric prometheus.Gauge
}{
{
expectedValue: float64(staticEpochInfo.AbsoluteSlot),
metric: confirmedSlotHeight,
},
{
expectedValue: float64(staticEpochInfo.TransactionCount),
metric: totalTransactionsTotal,
},
{
expectedValue: float64(staticEpochInfo.Epoch),
metric: currentEpochNumber,
},
{
expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex),
metric: epochFirstSlot,
},
{
expectedValue: float64(staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex + staticEpochInfo.SlotsInEpoch),
metric: epochLastSlot,
},
}

for _, testCase := range tests {
name := extractName(testCase.metric.Desc())
t.Run(
name,
func(t *testing.T) {
assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric))
},
)
}

hosts := []string{
"B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",
"C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",
"4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",
}
metrics := map[string]*prometheus.CounterVec{
"solana_leader_slots_total": leaderSlotsTotal,
"solana_leader_slots_by_epoch": leaderSlotsByEpoch,
}
statuses := []string{"valid", "skipped"}
for name, metric := range metrics {
// subtest for each metric:
t.Run(name, func(t *testing.T) {
for _, status := range statuses {
// sub subtest for each status (as each one requires a different calc)
t.Run(status, func(t *testing.T) {
for _, host := range hosts {
testBlockProductionMetric(t, metric, host, status)
}
})
}
})
}
}

func testBlockProductionMetric(
t *testing.T,
metric *prometheus.CounterVec,
host string,
status string,
) {
hostInfo := staticBlockProduction.Hosts[host]
// get expected value depending on status:
var expectedValue float64
switch status {
case "valid":
expectedValue = float64(hostInfo.BlocksProduced)
case "skipped":
expectedValue = float64(hostInfo.LeaderSlots - hostInfo.BlocksProduced)
}
// get labels (leaderSlotsByEpoch requires an extra one)
labels := []string{status, host}
if metric == leaderSlotsByEpoch {
labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch))
}
// now we can do the assertion:
assert.Equal(t, expectedValue, testutil.ToFloat64(metric.WithLabelValues(labels...)))
}
10 changes: 10 additions & 0 deletions cmd/solana_exporter/slots.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,21 @@ 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)
_, err = updateCounters(c.rpcClient, currentEpoch, watermark, &lastSlot)
if err != nil {
klog.Error(err)
}
ticker := time.NewTicker(slotPacerSchedule)

for {
Expand Down
138 changes: 138 additions & 0 deletions cmd/solana_exporter/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"context"
"github.com/certusone/solana_exporter/pkg/rpc"
"github.com/prometheus/client_golang/prometheus"
"regexp"
)

type (
staticRPCClient struct{}
// TODO: create dynamicRPCClient + according tests!
)

var (
staticEpochInfo = rpc.EpochInfo{
AbsoluteSlot: 166598,
BlockHeight: 166500,
Epoch: 27,
SlotIndex: 2790,
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
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{
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 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
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading

0 comments on commit f1e948f

Please sign in to comment.