refactored config
This commit is contained in:
parent
b2cde35375
commit
6f4fcad90d
|
@ -13,33 +13,45 @@ import (
|
|||
"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 (
|
||||
httpTimeout = 60 * time.Second
|
||||
rpcAddr = flag.String("rpcURI", "", "Solana RPC URI (including protocol and path)")
|
||||
addr = flag.String("addr", ":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 in seconds")
|
||||
rpcUrl = flag.String("rpc-url", "", "Solana RPC URI (including protocol and path)")
|
||||
listenAddress = flag.String("listen-address", ":8080", "Listen address")
|
||||
httpTimeoutSecs = flag.Int("http-timeout", 60, "HTTP timeout to use, in seconds.")
|
||||
|
||||
// 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(
|
||||
"balance-addresses",
|
||||
"",
|
||||
"Comma-separated list of addresses to monitor SOL balances.",
|
||||
)
|
||||
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.",
|
||||
"Comma-separated list of addresses to monitor SOL balances for, "+
|
||||
"in addition to the identity and vote accounts of the provided nodekeys.",
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -47,15 +59,12 @@ func init() {
|
|||
klog.InitFlags(nil)
|
||||
}
|
||||
|
||||
type solanaCollector struct {
|
||||
type SolanaCollector struct {
|
||||
rpcClient rpc.Provider
|
||||
|
||||
// config:
|
||||
slotPace time.Duration
|
||||
balanceAddresses []string
|
||||
leaderSlotAddresses []string
|
||||
inflationRewardAddresses []string
|
||||
feeRewardAddresses []string
|
||||
slotPace time.Duration
|
||||
balanceAddresses []string
|
||||
|
||||
/// descriptors:
|
||||
totalValidatorsDesc *prometheus.Desc
|
||||
|
@ -67,84 +76,60 @@ type solanaCollector struct {
|
|||
balances *prometheus.Desc
|
||||
}
|
||||
|
||||
func createSolanaCollector(
|
||||
provider rpc.Provider,
|
||||
slotPace time.Duration,
|
||||
balanceAddresses []string,
|
||||
leaderSlotAddresses []string,
|
||||
inflationRewardAddresses []string,
|
||||
feeRewardAddresses []string,
|
||||
) *solanaCollector {
|
||||
return &solanaCollector{
|
||||
rpcClient: provider,
|
||||
slotPace: slotPace,
|
||||
balanceAddresses: balanceAddresses,
|
||||
leaderSlotAddresses: leaderSlotAddresses,
|
||||
inflationRewardAddresses: inflationRewardAddresses,
|
||||
feeRewardAddresses: feeRewardAddresses,
|
||||
func NewSolanaCollector(
|
||||
provider rpc.Provider, slotPace time.Duration, balanceAddresses []string, nodekeys []string, votekeys []string,
|
||||
) *SolanaCollector {
|
||||
collector := &SolanaCollector{
|
||||
rpcClient: provider,
|
||||
slotPace: slotPace,
|
||||
balanceAddresses: CombineUnique(balanceAddresses, nodekeys, votekeys),
|
||||
totalValidatorsDesc: prometheus.NewDesc(
|
||||
"solana_active_validators",
|
||||
"Total number of active validators by state",
|
||||
[]string{"state"},
|
||||
[]string{StateLabel},
|
||||
nil,
|
||||
),
|
||||
validatorActivatedStake: prometheus.NewDesc(
|
||||
"solana_validator_activated_stake",
|
||||
"Activated stake per validator",
|
||||
[]string{"pubkey", "nodekey"},
|
||||
[]string{VotekeyLabel, NodekeyLabel},
|
||||
nil,
|
||||
),
|
||||
validatorLastVote: prometheus.NewDesc(
|
||||
"solana_validator_last_vote",
|
||||
"Last voted slot per validator",
|
||||
[]string{"pubkey", "nodekey"},
|
||||
[]string{VotekeyLabel, NodekeyLabel},
|
||||
nil,
|
||||
),
|
||||
validatorRootSlot: prometheus.NewDesc(
|
||||
"solana_validator_root_slot",
|
||||
"Root slot per validator",
|
||||
[]string{"pubkey", "nodekey"},
|
||||
[]string{VotekeyLabel, NodekeyLabel},
|
||||
nil,
|
||||
),
|
||||
validatorDelinquent: prometheus.NewDesc(
|
||||
"solana_validator_delinquent",
|
||||
"Whether a validator is delinquent",
|
||||
[]string{"pubkey", "nodekey"},
|
||||
[]string{VotekeyLabel, NodekeyLabel},
|
||||
nil,
|
||||
),
|
||||
solanaVersion: prometheus.NewDesc(
|
||||
"solana_node_version",
|
||||
"Node version of solana",
|
||||
[]string{"version"},
|
||||
[]string{VersionLabel},
|
||||
nil,
|
||||
),
|
||||
balances: prometheus.NewDesc(
|
||||
"solana_account_balance",
|
||||
"Solana account balances",
|
||||
[]string{"address"},
|
||||
[]string{AddressLabel},
|
||||
nil,
|
||||
),
|
||||
}
|
||||
return collector
|
||||
}
|
||||
|
||||
func NewSolanaCollector(
|
||||
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) {
|
||||
func (c *SolanaCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.totalValidatorsDesc
|
||||
ch <- c.solanaVersion
|
||||
ch <- c.validatorActivatedStake
|
||||
|
@ -154,8 +139,8 @@ func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) {
|
|||
ch <- c.balances
|
||||
}
|
||||
|
||||
func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||
voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, votePubkey)
|
||||
func (c *SolanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||
voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, nil)
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(c.totalValidatorsDesc, err)
|
||||
ch <- prometheus.NewInvalidMetric(c.validatorActivatedStake, err)
|
||||
|
@ -166,10 +151,10 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro
|
|||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), "delinquent",
|
||||
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), StateDelinquent,
|
||||
)
|
||||
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...) {
|
||||
|
@ -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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||
balances, err := fetchBalances(ctx, c.rpcClient, c.balanceAddresses)
|
||||
func (c *SolanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||
balances, err := FetchBalances(ctx, c.rpcClient, c.balanceAddresses)
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(c.solanaVersion, err)
|
||||
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) {
|
||||
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) {
|
||||
func (c *SolanaCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
@ -253,15 +226,16 @@ func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) {
|
|||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
flag.Parse()
|
||||
|
||||
if *rpcAddr == "" {
|
||||
if *rpcUrl == "" {
|
||||
klog.Fatal("Please specify -rpcURI")
|
||||
}
|
||||
|
||||
if *leaderSlotAddresses == "" {
|
||||
if *comprehensiveSlotTracking {
|
||||
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.",
|
||||
)
|
||||
}
|
||||
|
@ -269,37 +243,31 @@ func main() {
|
|||
httpTimeout = time.Duration(*httpTimeoutSecs) * time.Second
|
||||
|
||||
var (
|
||||
balAddresses []string
|
||||
lsAddresses []string
|
||||
irAddresses []string
|
||||
frAddresses []string
|
||||
balAddresses []string
|
||||
validatorNodekeys []string
|
||||
)
|
||||
if *balanceAddresses != "" {
|
||||
balAddresses = strings.Split(*balanceAddresses, ",")
|
||||
klog.Infof("Monitoring balances for %v", balAddresses)
|
||||
}
|
||||
if *leaderSlotAddresses != "" {
|
||||
lsAddresses = strings.Split(*leaderSlotAddresses, ",")
|
||||
klog.Infof("Monitoring leader-slot by epoch for %v", lsAddresses)
|
||||
|
||||
}
|
||||
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)
|
||||
if *nodekeys != "" {
|
||||
validatorNodekeys = strings.Split(*nodekeys, ",")
|
||||
klog.Infof("Monitoring the following validators: %v", validatorNodekeys)
|
||||
}
|
||||
|
||||
collector := NewSolanaCollector(*rpcAddr, balAddresses, lsAddresses, irAddresses, frAddresses)
|
||||
|
||||
slotWatcher := NewCollectorSlotWatcher(collector)
|
||||
go slotWatcher.WatchSlots(context.Background(), collector.slotPace)
|
||||
client := rpc.NewRPCClient(*rpcUrl)
|
||||
ctx_, cancel := context.WithTimeout(ctx, httpTimeout)
|
||||
defer cancel()
|
||||
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)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
klog.Infof("listening on %s", *addr)
|
||||
klog.Fatal(http.ListenAndServe(*addr, nil))
|
||||
klog.Infof("listening on %s", *listenAddress)
|
||||
klog.Fatal(http.ListenAndServe(*listenAddress, nil))
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ type (
|
|||
var (
|
||||
identities = []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"}
|
||||
nv = len(identities)
|
||||
staticEpochInfo = rpc.EpochInfo{
|
||||
|
@ -106,6 +106,16 @@ var (
|
|||
staticLeaderSchedule = map[string][]int64{
|
||||
"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) {
|
||||
collector := createSolanaCollector(
|
||||
&staticRPCClient{}, slotPacerSchedule, identities, []string{}, votekeys, identities,
|
||||
)
|
||||
collector := NewSolanaCollector(&staticRPCClient{}, slotPacerSchedule, nil, identities, votekeys)
|
||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||
|
||||
testCases := []collectionTest{
|
||||
|
@ -414,9 +422,9 @@ solana_active_validators{state="delinquent"} 1
|
|||
ExpectedResponse: `
|
||||
# HELP solana_validator_activated_stake Activated stake per validator
|
||||
# TYPE solana_validator_activated_stake gauge
|
||||
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49
|
||||
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42
|
||||
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
|
||||
solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 49
|
||||
solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 42
|
||||
solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 43
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -424,9 +432,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
|
|||
ExpectedResponse: `
|
||||
# HELP solana_validator_last_vote Last voted slot per validator
|
||||
# TYPE solana_validator_last_vote gauge
|
||||
solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92
|
||||
solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147
|
||||
solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
|
||||
solana_validator_last_vote{nodekey="aaa",votekey="AAA"} 92
|
||||
solana_validator_last_vote{nodekey="bbb",votekey="BBB"} 147
|
||||
solana_validator_last_vote{nodekey="ccc",votekey="CCC"} 148
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -434,9 +442,9 @@ solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
|
|||
ExpectedResponse: `
|
||||
# HELP solana_validator_root_slot Root slot per validator
|
||||
# TYPE solana_validator_root_slot gauge
|
||||
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3
|
||||
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18
|
||||
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
|
||||
solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 3
|
||||
solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 18
|
||||
solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 19
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -444,9 +452,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
|
|||
ExpectedResponse: `
|
||||
# HELP solana_validator_delinquent Whether a validator is delinquent
|
||||
# TYPE solana_validator_delinquent gauge
|
||||
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1
|
||||
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
|
||||
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
|
||||
solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 1
|
||||
solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 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",
|
||||
ExpectedResponse: `
|
||||
# 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
|
||||
`,
|
||||
Name: "solana_account_balance",
|
||||
ExpectedResponse: balanceMetricResponse,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -474,7 +476,7 @@ solana_account_balance{address="ccc"} 3
|
|||
|
||||
func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
|
||||
client := newDynamicRPCClient()
|
||||
collector := createSolanaCollector(client, slotPacerSchedule, identities, []string{}, votekeys, identities)
|
||||
collector := NewSolanaCollector(client, slotPacerSchedule, nil, identities, votekeys)
|
||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||
|
||||
// start off by testing initial state:
|
||||
|
@ -493,9 +495,9 @@ solana_active_validators{state="delinquent"} 0
|
|||
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
|
||||
solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 1000000
|
||||
solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 1000000
|
||||
solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -503,9 +505,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
|
|||
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
|
||||
solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0
|
||||
solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0
|
||||
solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -513,9 +515,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
|
|||
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
|
||||
solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0
|
||||
solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 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",
|
||||
ExpectedResponse: `
|
||||
# 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
|
||||
`,
|
||||
Name: "solana_account_balance",
|
||||
ExpectedResponse: balanceMetricResponse,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -562,9 +558,9 @@ solana_active_validators{state="delinquent"} 1
|
|||
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
|
||||
solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 2000000
|
||||
solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 500000
|
||||
solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -572,9 +568,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
|
|||
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
|
||||
solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0
|
||||
solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0
|
||||
solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -582,9 +578,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
|
|||
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
|
||||
solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0
|
||||
solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
|
||||
solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -596,14 +592,8 @@ solana_node_version{version="v1.2.3"} 1
|
|||
`,
|
||||
},
|
||||
{
|
||||
Name: "solana_account_balance",
|
||||
ExpectedResponse: `
|
||||
# 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
|
||||
`,
|
||||
Name: "solana_account_balance",
|
||||
ExpectedResponse: balanceMetricResponse,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ type SlotWatcher struct {
|
|||
client rpc.Provider
|
||||
|
||||
// config:
|
||||
leaderSlotAddresses []string
|
||||
inflationRewardAddresses []string
|
||||
feeRewardAddresses []string
|
||||
nodekeys []string
|
||||
votekeys []string
|
||||
comprehensiveSlotTracking bool
|
||||
|
||||
// currentEpoch is the current epoch we are watching
|
||||
currentEpoch int64
|
||||
|
@ -68,7 +68,7 @@ var (
|
|||
Name: "solana_leader_slots_total",
|
||||
Help: "(DEPRECATED) Number of leader slots per leader, grouped by skip status",
|
||||
},
|
||||
[]string{"status", "nodekey"},
|
||||
[]string{SkipStatusLabel, NodekeyLabel},
|
||||
)
|
||||
|
||||
leaderSlotsByEpoch = prometheus.NewCounterVec(
|
||||
|
@ -76,7 +76,7 @@ var (
|
|||
Name: "solana_leader_slots_by_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(
|
||||
|
@ -84,7 +84,7 @@ var (
|
|||
Name: "solana_inflation_rewards",
|
||||
Help: "Inflation reward earned per validator vote account, per epoch",
|
||||
},
|
||||
[]string{"votekey", "epoch"},
|
||||
[]string{VotekeyLabel, EpochLabel},
|
||||
)
|
||||
|
||||
feeRewards = prometheus.NewCounterVec(
|
||||
|
@ -92,16 +92,18 @@ var (
|
|||
Name: "solana_fee_rewards",
|
||||
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{
|
||||
client: collector.rpcClient,
|
||||
leaderSlotAddresses: collector.leaderSlotAddresses,
|
||||
inflationRewardAddresses: collector.inflationRewardAddresses,
|
||||
feeRewardAddresses: collector.feeRewardAddresses,
|
||||
client: client,
|
||||
nodekeys: nodekeys,
|
||||
votekeys: votekeys,
|
||||
comprehensiveSlotTracking: comprehensiveSlotTracking,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,8 +159,8 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) {
|
|||
}
|
||||
|
||||
if epochInfo.Epoch > c.currentEpoch {
|
||||
// if we have configured inflation reward addresses, fetch em
|
||||
if len(c.inflationRewardAddresses) > 0 {
|
||||
// fetch inflation rewards for vote accounts:
|
||||
if len(c.votekeys) > 0 {
|
||||
err = c.fetchAndEmitInflationRewards(ctx, c.currentEpoch)
|
||||
if err != nil {
|
||||
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)
|
||||
defer cancel()
|
||||
klog.Infof("Updating leader schedule for epoch %v ...", c.currentEpoch)
|
||||
leaderSchedule, err := GetTrimmedLeaderSchedule(
|
||||
ctx, c.client, c.feeRewardAddresses, epoch.AbsoluteSlot, c.firstSlot,
|
||||
)
|
||||
leaderSchedule, err := GetTrimmedLeaderSchedule(ctx, c.client, c.nodekeys, epoch.AbsoluteSlot, c.firstSlot)
|
||||
if err != nil {
|
||||
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)
|
||||
skipped := float64(production.LeaderSlots - production.BlocksProduced)
|
||||
|
||||
leaderSlotsTotal.WithLabelValues("valid", address).Add(valid)
|
||||
leaderSlotsTotal.WithLabelValues("skipped", address).Add(skipped)
|
||||
leaderSlotsTotal.WithLabelValues(StatusValid, address).Add(valid)
|
||||
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)
|
||||
leaderSlotsByEpoch.WithLabelValues("valid", address, epochStr).Add(valid)
|
||||
leaderSlotsByEpoch.WithLabelValues("skipped", address, epochStr).Add(skipped)
|
||||
leaderSlotsByEpoch.WithLabelValues(StatusValid, address, epochStr).Add(valid)
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
rewardInfos, err := c.client.GetInflationReward(
|
||||
ctx, rpc.CommitmentConfirmed, c.inflationRewardAddresses, &epoch, nil,
|
||||
)
|
||||
rewardInfos, err := c.client.GetInflationReward(ctx, rpc.CommitmentConfirmed, c.votekeys, &epoch, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching inflation rewards: %w", err)
|
||||
}
|
||||
|
||||
for i, rewardInfo := range rewardInfos {
|
||||
address := c.inflationRewardAddresses[i]
|
||||
address := c.votekeys[i]
|
||||
reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol)
|
||||
inflationRewards.WithLabelValues(address, toString(epoch)).Set(reward)
|
||||
}
|
||||
|
|
|
@ -92,10 +92,9 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) {
|
|||
leaderSlotsTotal.Reset()
|
||||
leaderSlotsByEpoch.Reset()
|
||||
|
||||
collector := createSolanaCollector(
|
||||
&staticRPCClient{}, 100*time.Millisecond, identities, []string{}, votekeys, identities,
|
||||
)
|
||||
watcher := NewCollectorSlotWatcher(collector)
|
||||
client := staticRPCClient{}
|
||||
collector := NewSolanaCollector(&client, 100*time.Millisecond, nil, identities, votekeys)
|
||||
watcher := NewSlotWatcher(&client, identities, votekeys, false)
|
||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
@ -163,8 +162,8 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) {
|
|||
|
||||
// create clients:
|
||||
client := newDynamicRPCClient()
|
||||
collector := createSolanaCollector(client, 300*time.Millisecond, identities, []string{}, votekeys, identities)
|
||||
watcher := NewCollectorSlotWatcher(collector)
|
||||
collector := NewSolanaCollector(client, 300*time.Millisecond, nil, identities, votekeys)
|
||||
watcher := NewSlotWatcher(client, identities, votekeys, false)
|
||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||
|
||||
// start client/collector and wait a bit:
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"github.com/asymmetric-research/solana_exporter/pkg/rpc"
|
||||
"k8s.io/klog/v2"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func assertf(condition bool, format string, args ...any) {
|
||||
|
@ -60,3 +61,55 @@ func GetTrimmedLeaderSchedule(
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue