refactored config

This commit is contained in:
Matt Johnstone 2024-10-14 14:39:41 +02:00
parent b2cde35375
commit 6f4fcad90d
No known key found for this signature in database
GPG Key ID: BE985FBB9BE7D3BB
6 changed files with 234 additions and 203 deletions

View File

@ -13,33 +13,45 @@ import (
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
const (
SkipStatusLabel = "status"
StateLabel = "state"
NodekeyLabel = "nodekey"
VotekeyLabel = "votekey"
VersionLabel = "version"
AddressLabel = "address"
EpochLabel = "epoch"
StatusSkipped = "skipped"
StatusValid = "valid"
StateCurrent = "current"
StateDelinquent = "delinquent"
)
var ( var (
httpTimeout = 60 * time.Second httpTimeout = 60 * time.Second
rpcAddr = flag.String("rpcURI", "", "Solana RPC URI (including protocol and path)") rpcUrl = flag.String("rpc-url", "", "Solana RPC URI (including protocol and path)")
addr = flag.String("addr", ":8080", "Listen address") listenAddress = flag.String("listen-address", ":8080", "Listen address")
votePubkey = flag.String("votepubkey", "", "Validator vote address (will only return results of this address)") httpTimeoutSecs = flag.Int("http-timeout", 60, "HTTP timeout to use, in seconds.")
httpTimeoutSecs = flag.Int("http_timeout", 60, "HTTP timeout in seconds")
// addresses: // addresses:
nodekeys = flag.String(
"nodekeys",
"",
"Comma-separated list of nodekeys (identity accounts) representing validators to monitor.",
)
comprehensiveSlotTracking = flag.Bool(
"comprehensive-slot-tracking",
false,
"Set this flag to track solana_leader_slots_by_epoch for ALL validators. "+
"Warning: this will lead to potentially thousands of new Prometheus metrics being created every epoch.",
)
balanceAddresses = flag.String( balanceAddresses = flag.String(
"balance-addresses", "balance-addresses",
"", "",
"Comma-separated list of addresses to monitor SOL balances.", "Comma-separated list of addresses to monitor SOL balances for, "+
) "in addition to the identity and vote accounts of the provided nodekeys.",
leaderSlotAddresses = flag.String(
"leader-slot-addresses",
"",
"Comma-separated list of addresses to monitor leader slots by epoch for, leave nil to track by epoch for all validators (this creates a lot of Prometheus metrics with every new epoch).",
)
inflationRewardAddresses = flag.String(
"inflation-reward-addresses",
"",
"Comma-separated list of validator vote accounts to track inflationary rewards for",
)
feeRewardAddresses = flag.String(
"fee-reward-addresses",
"",
"Comma-separated list of validator identity accounts to track fee rewards for.",
) )
) )
@ -47,15 +59,12 @@ func init() {
klog.InitFlags(nil) klog.InitFlags(nil)
} }
type solanaCollector struct { type SolanaCollector struct {
rpcClient rpc.Provider rpcClient rpc.Provider
// config: // config:
slotPace time.Duration slotPace time.Duration
balanceAddresses []string balanceAddresses []string
leaderSlotAddresses []string
inflationRewardAddresses []string
feeRewardAddresses []string
/// descriptors: /// descriptors:
totalValidatorsDesc *prometheus.Desc totalValidatorsDesc *prometheus.Desc
@ -67,84 +76,60 @@ type solanaCollector struct {
balances *prometheus.Desc balances *prometheus.Desc
} }
func createSolanaCollector( func NewSolanaCollector(
provider rpc.Provider, provider rpc.Provider, slotPace time.Duration, balanceAddresses []string, nodekeys []string, votekeys []string,
slotPace time.Duration, ) *SolanaCollector {
balanceAddresses []string, collector := &SolanaCollector{
leaderSlotAddresses []string, rpcClient: provider,
inflationRewardAddresses []string, slotPace: slotPace,
feeRewardAddresses []string, balanceAddresses: CombineUnique(balanceAddresses, nodekeys, votekeys),
) *solanaCollector {
return &solanaCollector{
rpcClient: provider,
slotPace: slotPace,
balanceAddresses: balanceAddresses,
leaderSlotAddresses: leaderSlotAddresses,
inflationRewardAddresses: inflationRewardAddresses,
feeRewardAddresses: feeRewardAddresses,
totalValidatorsDesc: prometheus.NewDesc( totalValidatorsDesc: prometheus.NewDesc(
"solana_active_validators", "solana_active_validators",
"Total number of active validators by state", "Total number of active validators by state",
[]string{"state"}, []string{StateLabel},
nil, nil,
), ),
validatorActivatedStake: prometheus.NewDesc( validatorActivatedStake: prometheus.NewDesc(
"solana_validator_activated_stake", "solana_validator_activated_stake",
"Activated stake per validator", "Activated stake per validator",
[]string{"pubkey", "nodekey"}, []string{VotekeyLabel, NodekeyLabel},
nil, nil,
), ),
validatorLastVote: prometheus.NewDesc( validatorLastVote: prometheus.NewDesc(
"solana_validator_last_vote", "solana_validator_last_vote",
"Last voted slot per validator", "Last voted slot per validator",
[]string{"pubkey", "nodekey"}, []string{VotekeyLabel, NodekeyLabel},
nil, nil,
), ),
validatorRootSlot: prometheus.NewDesc( validatorRootSlot: prometheus.NewDesc(
"solana_validator_root_slot", "solana_validator_root_slot",
"Root slot per validator", "Root slot per validator",
[]string{"pubkey", "nodekey"}, []string{VotekeyLabel, NodekeyLabel},
nil, nil,
), ),
validatorDelinquent: prometheus.NewDesc( validatorDelinquent: prometheus.NewDesc(
"solana_validator_delinquent", "solana_validator_delinquent",
"Whether a validator is delinquent", "Whether a validator is delinquent",
[]string{"pubkey", "nodekey"}, []string{VotekeyLabel, NodekeyLabel},
nil, nil,
), ),
solanaVersion: prometheus.NewDesc( solanaVersion: prometheus.NewDesc(
"solana_node_version", "solana_node_version",
"Node version of solana", "Node version of solana",
[]string{"version"}, []string{VersionLabel},
nil, nil,
), ),
balances: prometheus.NewDesc( balances: prometheus.NewDesc(
"solana_account_balance", "solana_account_balance",
"Solana account balances", "Solana account balances",
[]string{"address"}, []string{AddressLabel},
nil, nil,
), ),
} }
return collector
} }
func NewSolanaCollector( func (c *SolanaCollector) Describe(ch chan<- *prometheus.Desc) {
rpcAddr string,
balanceAddresses []string,
leaderSlotAddresses []string,
inflationRewardAddresses []string,
feeRewardAddresses []string,
) *solanaCollector {
return createSolanaCollector(
rpc.NewRPCClient(rpcAddr),
slotPacerSchedule,
balanceAddresses,
leaderSlotAddresses,
inflationRewardAddresses,
feeRewardAddresses,
)
}
func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.totalValidatorsDesc ch <- c.totalValidatorsDesc
ch <- c.solanaVersion ch <- c.solanaVersion
ch <- c.validatorActivatedStake ch <- c.validatorActivatedStake
@ -154,8 +139,8 @@ func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.balances ch <- c.balances
} }
func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) { func (c *SolanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) {
voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, votePubkey) voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, nil)
if err != nil { if err != nil {
ch <- prometheus.NewInvalidMetric(c.totalValidatorsDesc, err) ch <- prometheus.NewInvalidMetric(c.totalValidatorsDesc, err)
ch <- prometheus.NewInvalidMetric(c.validatorActivatedStake, err) ch <- prometheus.NewInvalidMetric(c.validatorActivatedStake, err)
@ -166,10 +151,10 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro
} }
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), "delinquent", c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), StateDelinquent,
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Current)), "current", c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Current)), StateCurrent,
) )
for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) { for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) {
@ -208,7 +193,7 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro
} }
} }
func (c *solanaCollector) collectVersion(ctx context.Context, ch chan<- prometheus.Metric) { func (c *SolanaCollector) collectVersion(ctx context.Context, ch chan<- prometheus.Metric) {
version, err := c.rpcClient.GetVersion(ctx) version, err := c.rpcClient.GetVersion(ctx)
if err != nil { if err != nil {
@ -219,8 +204,8 @@ func (c *solanaCollector) collectVersion(ctx context.Context, ch chan<- promethe
ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version) ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version)
} }
func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) { func (c *SolanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) {
balances, err := fetchBalances(ctx, c.rpcClient, c.balanceAddresses) balances, err := FetchBalances(ctx, c.rpcClient, c.balanceAddresses)
if err != nil { if err != nil {
ch <- prometheus.NewInvalidMetric(c.solanaVersion, err) ch <- prometheus.NewInvalidMetric(c.solanaVersion, err)
return return
@ -231,19 +216,7 @@ func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometh
} }
} }
func fetchBalances(ctx context.Context, client rpc.Provider, addresses []string) (map[string]float64, error) { func (c *SolanaCollector) Collect(ch chan<- prometheus.Metric) {
balances := make(map[string]float64)
for _, address := range addresses {
balance, err := client.GetBalance(ctx, rpc.CommitmentConfirmed, address)
if err != nil {
return nil, err
}
balances[address] = balance
}
return balances, nil
}
func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) {
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
defer cancel() defer cancel()
@ -253,15 +226,16 @@ func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) {
} }
func main() { func main() {
ctx := context.Background()
flag.Parse() flag.Parse()
if *rpcAddr == "" { if *rpcUrl == "" {
klog.Fatal("Please specify -rpcURI") klog.Fatal("Please specify -rpcURI")
} }
if *leaderSlotAddresses == "" { if *comprehensiveSlotTracking {
klog.Warning( klog.Warning(
"Not specifying leader-slot-addresses will lead to potentially thousands of new " + "Comprehensive slot tracking will lead to potentially thousands of new " +
"Prometheus metrics being created every epoch.", "Prometheus metrics being created every epoch.",
) )
} }
@ -269,37 +243,31 @@ func main() {
httpTimeout = time.Duration(*httpTimeoutSecs) * time.Second httpTimeout = time.Duration(*httpTimeoutSecs) * time.Second
var ( var (
balAddresses []string balAddresses []string
lsAddresses []string validatorNodekeys []string
irAddresses []string
frAddresses []string
) )
if *balanceAddresses != "" { if *balanceAddresses != "" {
balAddresses = strings.Split(*balanceAddresses, ",") balAddresses = strings.Split(*balanceAddresses, ",")
klog.Infof("Monitoring balances for %v", balAddresses)
} }
if *leaderSlotAddresses != "" { if *nodekeys != "" {
lsAddresses = strings.Split(*leaderSlotAddresses, ",") validatorNodekeys = strings.Split(*nodekeys, ",")
klog.Infof("Monitoring leader-slot by epoch for %v", lsAddresses) klog.Infof("Monitoring the following validators: %v", validatorNodekeys)
}
if *inflationRewardAddresses != "" {
irAddresses = strings.Split(*inflationRewardAddresses, ",")
klog.Infof("Monitoring inflation reward by epoch for %v", irAddresses)
}
if *feeRewardAddresses != "" {
frAddresses = strings.Split(*feeRewardAddresses, ",")
klog.Infof("Monitoring fee reward by epoch for %v", frAddresses)
} }
collector := NewSolanaCollector(*rpcAddr, balAddresses, lsAddresses, irAddresses, frAddresses) client := rpc.NewRPCClient(*rpcUrl)
ctx_, cancel := context.WithTimeout(ctx, httpTimeout)
slotWatcher := NewCollectorSlotWatcher(collector) defer cancel()
go slotWatcher.WatchSlots(context.Background(), collector.slotPace) votekeys, err := GetAssociatedVoteAccounts(ctx_, client, rpc.CommitmentFinalized, validatorNodekeys)
if err != nil {
klog.Fatalf("Failed to get associated vote accounts for %v: %v", nodekeys, err)
}
collector := NewSolanaCollector(client, slotPacerSchedule, balAddresses, validatorNodekeys, votekeys)
slotWatcher := NewSlotWatcher(client, validatorNodekeys, votekeys, *comprehensiveSlotTracking)
go slotWatcher.WatchSlots(ctx, collector.slotPace)
prometheus.MustRegister(collector) prometheus.MustRegister(collector)
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
klog.Infof("listening on %s", *addr) klog.Infof("listening on %s", *listenAddress)
klog.Fatal(http.ListenAndServe(*addr, nil)) klog.Fatal(http.ListenAndServe(*listenAddress, nil))
} }

View File

@ -43,7 +43,7 @@ type (
var ( var (
identities = []string{"aaa", "bbb", "ccc"} identities = []string{"aaa", "bbb", "ccc"}
votekeys = []string{"AAA", "BBB", "CCC"} votekeys = []string{"AAA", "BBB", "CCC"}
balances = map[string]float64{"aaa": 1, "bbb": 2, "ccc": 3} balances = map[string]float64{"aaa": 1, "bbb": 2, "ccc": 3, "AAA": 4, "BBB": 5, "CCC": 6}
identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"} identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"}
nv = len(identities) nv = len(identities)
staticEpochInfo = rpc.EpochInfo{ staticEpochInfo = rpc.EpochInfo{
@ -106,6 +106,16 @@ var (
staticLeaderSchedule = map[string][]int64{ staticLeaderSchedule = map[string][]int64{
"aaa": {0, 3, 6, 9, 12}, "bbb": {1, 4, 7, 10, 13}, "ccc": {2, 5, 8, 11, 14}, "aaa": {0, 3, 6, 9, 12}, "bbb": {1, 4, 7, 10, 13}, "ccc": {2, 5, 8, 11, 14},
} }
balanceMetricResponse = `
# HELP solana_account_balance Solana account balances
# TYPE solana_account_balance gauge
solana_account_balance{address="AAA"} 4
solana_account_balance{address="BBB"} 5
solana_account_balance{address="CCC"} 6
solana_account_balance{address="aaa"} 1
solana_account_balance{address="bbb"} 2
solana_account_balance{address="ccc"} 3
`
) )
/* /*
@ -394,9 +404,7 @@ func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases
} }
func TestSolanaCollector_Collect_Static(t *testing.T) { func TestSolanaCollector_Collect_Static(t *testing.T) {
collector := createSolanaCollector( collector := NewSolanaCollector(&staticRPCClient{}, slotPacerSchedule, nil, identities, votekeys)
&staticRPCClient{}, slotPacerSchedule, identities, []string{}, votekeys, identities,
)
prometheus.NewPedanticRegistry().MustRegister(collector) prometheus.NewPedanticRegistry().MustRegister(collector)
testCases := []collectionTest{ testCases := []collectionTest{
@ -414,9 +422,9 @@ solana_active_validators{state="delinquent"} 1
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator # HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge # TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49 solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 49
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42 solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 42
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43 solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 43
`, `,
}, },
{ {
@ -424,9 +432,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_last_vote Last voted slot per validator # HELP solana_validator_last_vote Last voted slot per validator
# TYPE solana_validator_last_vote gauge # TYPE solana_validator_last_vote gauge
solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92 solana_validator_last_vote{nodekey="aaa",votekey="AAA"} 92
solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147 solana_validator_last_vote{nodekey="bbb",votekey="BBB"} 147
solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148 solana_validator_last_vote{nodekey="ccc",votekey="CCC"} 148
`, `,
}, },
{ {
@ -434,9 +442,9 @@ solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator # HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge # TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3 solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 3
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18 solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 18
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19 solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 19
`, `,
}, },
{ {
@ -444,9 +452,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent # HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge # TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1 solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 1
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0 solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 0
`, `,
}, },
{ {
@ -458,14 +466,8 @@ solana_node_version{version="1.16.7"} 1
`, `,
}, },
{ {
Name: "solana_account_balance", Name: "solana_account_balance",
ExpectedResponse: ` ExpectedResponse: balanceMetricResponse,
# HELP solana_account_balance Solana account balances
# TYPE solana_account_balance gauge
solana_account_balance{address="aaa"} 1
solana_account_balance{address="bbb"} 2
solana_account_balance{address="ccc"} 3
`,
}, },
} }
@ -474,7 +476,7 @@ solana_account_balance{address="ccc"} 3
func TestSolanaCollector_Collect_Dynamic(t *testing.T) { func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
client := newDynamicRPCClient() client := newDynamicRPCClient()
collector := createSolanaCollector(client, slotPacerSchedule, identities, []string{}, votekeys, identities) collector := NewSolanaCollector(client, slotPacerSchedule, nil, identities, votekeys)
prometheus.NewPedanticRegistry().MustRegister(collector) prometheus.NewPedanticRegistry().MustRegister(collector)
// start off by testing initial state: // start off by testing initial state:
@ -493,9 +495,9 @@ solana_active_validators{state="delinquent"} 0
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator # HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge # TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 1000000 solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 1000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 1000000 solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 1000000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000 solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000
`, `,
}, },
{ {
@ -503,9 +505,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator # HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge # TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0 solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0 solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0 solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0
`, `,
}, },
{ {
@ -513,9 +515,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent # HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge # TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0 solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0 solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 0
`, `,
}, },
{ {
@ -527,14 +529,8 @@ solana_node_version{version="v1.0.0"} 1
`, `,
}, },
{ {
Name: "solana_account_balance", Name: "solana_account_balance",
ExpectedResponse: ` ExpectedResponse: balanceMetricResponse,
# HELP solana_account_balance Solana account balances
# TYPE solana_account_balance gauge
solana_account_balance{address="aaa"} 1
solana_account_balance{address="bbb"} 2
solana_account_balance{address="ccc"} 3
`,
}, },
} }
@ -562,9 +558,9 @@ solana_active_validators{state="delinquent"} 1
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator # HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge # TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 2000000 solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 2000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 500000 solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 500000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000 solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000
`, `,
}, },
{ {
@ -572,9 +568,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator # HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge # TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0 solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0 solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0 solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0
`, `,
}, },
{ {
@ -582,9 +578,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
ExpectedResponse: ` ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent # HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge # TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0 solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0 solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 1 solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 1
`, `,
}, },
{ {
@ -596,14 +592,8 @@ solana_node_version{version="v1.2.3"} 1
`, `,
}, },
{ {
Name: "solana_account_balance", Name: "solana_account_balance",
ExpectedResponse: ` ExpectedResponse: balanceMetricResponse,
# HELP solana_account_balance Solana account balances
# TYPE solana_account_balance gauge
solana_account_balance{address="aaa"} 1
solana_account_balance{address="bbb"} 2
solana_account_balance{address="ccc"} 3
`,
}, },
} }

View File

@ -21,9 +21,9 @@ type SlotWatcher struct {
client rpc.Provider client rpc.Provider
// config: // config:
leaderSlotAddresses []string nodekeys []string
inflationRewardAddresses []string votekeys []string
feeRewardAddresses []string comprehensiveSlotTracking bool
// currentEpoch is the current epoch we are watching // currentEpoch is the current epoch we are watching
currentEpoch int64 currentEpoch int64
@ -68,7 +68,7 @@ var (
Name: "solana_leader_slots_total", Name: "solana_leader_slots_total",
Help: "(DEPRECATED) Number of leader slots per leader, grouped by skip status", Help: "(DEPRECATED) Number of leader slots per leader, grouped by skip status",
}, },
[]string{"status", "nodekey"}, []string{SkipStatusLabel, NodekeyLabel},
) )
leaderSlotsByEpoch = prometheus.NewCounterVec( leaderSlotsByEpoch = prometheus.NewCounterVec(
@ -76,7 +76,7 @@ var (
Name: "solana_leader_slots_by_epoch", Name: "solana_leader_slots_by_epoch",
Help: "Number of leader slots per leader, grouped by skip status and epoch", Help: "Number of leader slots per leader, grouped by skip status and epoch",
}, },
[]string{"status", "nodekey", "epoch"}, []string{SkipStatusLabel, NodekeyLabel, EpochLabel},
) )
inflationRewards = prometheus.NewGaugeVec( inflationRewards = prometheus.NewGaugeVec(
@ -84,7 +84,7 @@ var (
Name: "solana_inflation_rewards", Name: "solana_inflation_rewards",
Help: "Inflation reward earned per validator vote account, per epoch", Help: "Inflation reward earned per validator vote account, per epoch",
}, },
[]string{"votekey", "epoch"}, []string{VotekeyLabel, EpochLabel},
) )
feeRewards = prometheus.NewCounterVec( feeRewards = prometheus.NewCounterVec(
@ -92,16 +92,18 @@ var (
Name: "solana_fee_rewards", Name: "solana_fee_rewards",
Help: "Transaction fee rewards earned per validator identity account, per epoch", Help: "Transaction fee rewards earned per validator identity account, per epoch",
}, },
[]string{"nodekey", "epoch"}, []string{NodekeyLabel, EpochLabel},
) )
) )
func NewCollectorSlotWatcher(collector *solanaCollector) *SlotWatcher { func NewSlotWatcher(
client rpc.Provider, nodekeys []string, votekeys []string, comprehensiveSlotTracking bool,
) *SlotWatcher {
return &SlotWatcher{ return &SlotWatcher{
client: collector.rpcClient, client: client,
leaderSlotAddresses: collector.leaderSlotAddresses, nodekeys: nodekeys,
inflationRewardAddresses: collector.inflationRewardAddresses, votekeys: votekeys,
feeRewardAddresses: collector.feeRewardAddresses, comprehensiveSlotTracking: comprehensiveSlotTracking,
} }
} }
@ -157,8 +159,8 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) {
} }
if epochInfo.Epoch > c.currentEpoch { if epochInfo.Epoch > c.currentEpoch {
// if we have configured inflation reward addresses, fetch em // fetch inflation rewards for vote accounts:
if len(c.inflationRewardAddresses) > 0 { if len(c.votekeys) > 0 {
err = c.fetchAndEmitInflationRewards(ctx, c.currentEpoch) err = c.fetchAndEmitInflationRewards(ctx, c.currentEpoch)
if err != nil { if err != nil {
klog.Errorf("Failed to emit inflation rewards, bailing out: %v", err) klog.Errorf("Failed to emit inflation rewards, bailing out: %v", err)
@ -222,9 +224,7 @@ func (c *SlotWatcher) trackEpoch(ctx context.Context, epoch *rpc.EpochInfo) {
ctx, cancel := context.WithTimeout(ctx, httpTimeout) ctx, cancel := context.WithTimeout(ctx, httpTimeout)
defer cancel() defer cancel()
klog.Infof("Updating leader schedule for epoch %v ...", c.currentEpoch) klog.Infof("Updating leader schedule for epoch %v ...", c.currentEpoch)
leaderSchedule, err := GetTrimmedLeaderSchedule( leaderSchedule, err := GetTrimmedLeaderSchedule(ctx, c.client, c.nodekeys, epoch.AbsoluteSlot, c.firstSlot)
ctx, c.client, c.feeRewardAddresses, epoch.AbsoluteSlot, c.firstSlot,
)
if err != nil { if err != nil {
klog.Errorf("Failed to get trimmed leader schedule, bailing out: %v", err) klog.Errorf("Failed to get trimmed leader schedule, bailing out: %v", err)
} }
@ -286,13 +286,13 @@ func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, endSlot i
valid := float64(production.BlocksProduced) valid := float64(production.BlocksProduced)
skipped := float64(production.LeaderSlots - production.BlocksProduced) skipped := float64(production.LeaderSlots - production.BlocksProduced)
leaderSlotsTotal.WithLabelValues("valid", address).Add(valid) leaderSlotsTotal.WithLabelValues(StatusValid, address).Add(valid)
leaderSlotsTotal.WithLabelValues("skipped", address).Add(skipped) leaderSlotsTotal.WithLabelValues(StatusSkipped, address).Add(skipped)
if len(c.leaderSlotAddresses) == 0 || slices.Contains(c.leaderSlotAddresses, address) { if slices.Contains(c.nodekeys, address) || c.comprehensiveSlotTracking {
epochStr := toString(c.currentEpoch) epochStr := toString(c.currentEpoch)
leaderSlotsByEpoch.WithLabelValues("valid", address, epochStr).Add(valid) leaderSlotsByEpoch.WithLabelValues(StatusValid, address, epochStr).Add(valid)
leaderSlotsByEpoch.WithLabelValues("skipped", address, epochStr).Add(skipped) leaderSlotsByEpoch.WithLabelValues(StatusSkipped, address, epochStr).Add(skipped)
} }
} }
@ -376,15 +376,13 @@ func (c *SlotWatcher) fetchAndEmitInflationRewards(ctx context.Context, epoch in
ctx, cancel := context.WithTimeout(ctx, httpTimeout) ctx, cancel := context.WithTimeout(ctx, httpTimeout)
defer cancel() defer cancel()
rewardInfos, err := c.client.GetInflationReward( rewardInfos, err := c.client.GetInflationReward(ctx, rpc.CommitmentConfirmed, c.votekeys, &epoch, nil)
ctx, rpc.CommitmentConfirmed, c.inflationRewardAddresses, &epoch, nil,
)
if err != nil { if err != nil {
return fmt.Errorf("error fetching inflation rewards: %w", err) return fmt.Errorf("error fetching inflation rewards: %w", err)
} }
for i, rewardInfo := range rewardInfos { for i, rewardInfo := range rewardInfos {
address := c.inflationRewardAddresses[i] address := c.votekeys[i]
reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol) reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol)
inflationRewards.WithLabelValues(address, toString(epoch)).Set(reward) inflationRewards.WithLabelValues(address, toString(epoch)).Set(reward)
} }

View File

@ -92,10 +92,9 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) {
leaderSlotsTotal.Reset() leaderSlotsTotal.Reset()
leaderSlotsByEpoch.Reset() leaderSlotsByEpoch.Reset()
collector := createSolanaCollector( client := staticRPCClient{}
&staticRPCClient{}, 100*time.Millisecond, identities, []string{}, votekeys, identities, collector := NewSolanaCollector(&client, 100*time.Millisecond, nil, identities, votekeys)
) watcher := NewSlotWatcher(&client, identities, votekeys, false)
watcher := NewCollectorSlotWatcher(collector)
prometheus.NewPedanticRegistry().MustRegister(collector) prometheus.NewPedanticRegistry().MustRegister(collector)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -163,8 +162,8 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) {
// create clients: // create clients:
client := newDynamicRPCClient() client := newDynamicRPCClient()
collector := createSolanaCollector(client, 300*time.Millisecond, identities, []string{}, votekeys, identities) collector := NewSolanaCollector(client, 300*time.Millisecond, nil, identities, votekeys)
watcher := NewCollectorSlotWatcher(collector) watcher := NewSlotWatcher(client, identities, votekeys, false)
prometheus.NewPedanticRegistry().MustRegister(collector) prometheus.NewPedanticRegistry().MustRegister(collector)
// start client/collector and wait a bit: // start client/collector and wait a bit:

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/asymmetric-research/solana_exporter/pkg/rpc" "github.com/asymmetric-research/solana_exporter/pkg/rpc"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"slices"
) )
func assertf(condition bool, format string, args ...any) { func assertf(condition bool, format string, args ...any) {
@ -60,3 +61,55 @@ func GetTrimmedLeaderSchedule(
return trimmedLeaderSchedule, nil return trimmedLeaderSchedule, nil
} }
// GetAssociatedVoteAccounts returns the votekeys associated with a given list of nodekeys
func GetAssociatedVoteAccounts(
ctx context.Context, client rpc.Provider, commitment rpc.Commitment, nodekeys []string,
) ([]string, error) {
voteAccounts, err := client.GetVoteAccounts(ctx, commitment, nil)
if err != nil {
return nil, err
}
// first map nodekey -> votekey:
voteAccountsMap := make(map[string]string)
for _, voteAccount := range append(voteAccounts.Current, voteAccounts.Delinquent...) {
voteAccountsMap[voteAccount.NodePubkey] = voteAccount.VotePubkey
}
votekeys := make([]string, len(nodekeys))
for i, nodeKey := range nodekeys {
votekey := voteAccountsMap[nodeKey]
if votekey == "" {
return nil, fmt.Errorf("failed to find vote key for node %v", nodeKey)
}
votekeys[i] = votekey
}
return votekeys, nil
}
// FetchBalances fetches SOL balances for a list of addresses
func FetchBalances(ctx context.Context, client rpc.Provider, addresses []string) (map[string]float64, error) {
balances := make(map[string]float64)
for _, address := range addresses {
balance, err := client.GetBalance(ctx, rpc.CommitmentConfirmed, address)
if err != nil {
return nil, err
}
balances[address] = balance
}
return balances, nil
}
// CombineUnique combines unique items from multiple arrays to a single array.
func CombineUnique[T comparable](args ...[]T) []T {
var uniqueItems []T
for _, arg := range args {
for _, item := range arg {
if !slices.Contains(uniqueItems, item) {
uniqueItems = append(uniqueItems, item)
}
}
}
return uniqueItems
}

View File

@ -22,3 +22,26 @@ func TestGetTrimmedLeaderSchedule(t *testing.T) {
assert.Equal(t, map[string][]int64{"aaa": {10, 13, 16, 19, 22}, "bbb": {11, 14, 17, 20, 23}}, schedule) assert.Equal(t, map[string][]int64{"aaa": {10, 13, 16, 19, 22}, "bbb": {11, 14, 17, 20, 23}}, schedule)
} }
func TestCombineUnique(t *testing.T) {
var (
v1 = []string{"1", "2", "3"}
v2 = []string{"2", "3", "4"}
v3 = []string{"3", "4", "5"}
)
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, CombineUnique(v1, v2, v3))
assert.Equal(t, []string{"2", "3", "4", "5"}, CombineUnique(nil, v2, v3))
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, CombineUnique(v1, nil, v3))
}
func TestFetchBalances(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client := staticRPCClient{}
fetchedBalances, err := FetchBalances(ctx, &client, CombineUnique(identities, votekeys))
assert.NoError(t, err)
assert.Equal(t, balances, fetchedBalances)
}