Merge pull request #69 from asymmetric-research/running-updates

WIP: Running updates
This commit is contained in:
Matt Johnstone 2024-11-05 20:56:37 +02:00 committed by GitHub
commit 49486bcc92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 82 additions and 63 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
# for prometheus configs: # for prometheus:
.prometheus .prometheus
# builds: # builds:

View File

@ -48,7 +48,8 @@ The exporter is configured via the following command line arguments:
### Notes on Configuration ### Notes on Configuration
* `-light-mode` is incompatible with both `-monitor-block-sizes` and `-comprehensive-slot-tracking`. * `-light-mode` is incompatible with `-nodekey`, `-balance-address`, `-monitor-block-sizes`, and
`-comprehensive-slot-tracking`, as these options control metrics which are not monitored in `-light-mode`.
* ***WARNING***: * ***WARNING***:
* Configuring `-comprehensive-slot-tracking` will lead to potentially thousands of new Prometheus metrics being * Configuring `-comprehensive-slot-tracking` will lead to potentially thousands of new Prometheus metrics being
created every epoch. created every epoch.
@ -75,17 +76,17 @@ The table below describes all the metrics collected by the `solana_exporter`:
| `solana_node_num_slots_behind` | The number of slots that the node is behind the latest cluster confirmed slot.* | N/A | | `solana_node_num_slots_behind` | The number of slots that the node is behind the latest cluster confirmed slot.* | N/A |
| `solana_node_minimum_ledger_slot` | The lowest slot that the node has information about in its ledger.* | N/A | | `solana_node_minimum_ledger_slot` | The lowest slot that the node has information about in its ledger.* | N/A |
| `solana_node_first_available_block` | The slot of the lowest confirmed block that has not been purged from the node's ledger.* | N/A | | `solana_node_first_available_block` | The slot of the lowest confirmed block that has not been purged from the node's ledger.* | N/A |
| `solana_total_transactions` | Total number of transactions processed without error since genesis.* | N/A | | `solana_node_total_transactions` | Total number of transactions processed without error since genesis.* | N/A |
| `solana_slot_height` | The current slot number.* | N/A | | `solana_node_slot_height` | The current slot number.* | N/A |
| `solana_epoch_number` | The current epoch number.* | N/A | | `solana_node_epoch_number` | The current epoch number.* | N/A |
| `solana_epoch_first_slot` | Current epoch's first slot \[inclusive\].* | N/A | | `solana_node_epoch_first_slot` | Current epoch's first slot \[inclusive\].* | N/A |
| `solana_epoch_last_slot` | Current epoch's last slot \[inclusive\].* | N/A | | `solana_node_epoch_last_slot` | Current epoch's last slot \[inclusive\].* | N/A |
| `solana_leader_slots` | Number of slots processed. | `status`, `nodekey` | | `solana_node_leader_slots` | Number of slots processed. | `status`, `nodekey` |
| `solana_leader_slots_by_epoch` | Number of slots processed. | `status`, `nodekey`, `epoch` | | `solana_node_leader_slots_by_epoch` | Number of slots processed. | `status`, `nodekey`, `epoch` |
| `solana_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` | | `solana_node_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` |
| `solana_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` | | `solana_node_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` |
| `solana_block_size` | Number of transactions per block.* | `nodekey`, `transaction_type` | | `solana_node_block_size` | Number of transactions per block. | `nodekey`, `transaction_type` |
| `solana_block_height` | The current block height of the node. | N/A | | `solana_node_block_height` | The current block height of the node.* | N/A |
***NOTE***: An `*` in the description indicates that the metric **is** tracked in `-light-mode`. ***NOTE***: An `*` in the description indicates that the metric **is** tracked in `-light-mode`.

View File

@ -59,8 +59,22 @@ func NewExporterConfig(
"monitorBlockSizes", monitorBlockSizes, "monitorBlockSizes", monitorBlockSizes,
"lightMode", lightMode, "lightMode", lightMode,
) )
if lightMode && (monitorBlockSizes || comprehensiveSlotTracking) { if lightMode {
return nil, fmt.Errorf("-light-mode is not compatible with -comprehensiveSlotTracking or -monitorBlockSizes") if comprehensiveSlotTracking {
return nil, fmt.Errorf("'-light-mode' is imcompatible with `-comprehensive-slot-tracking`")
}
if monitorBlockSizes {
return nil, fmt.Errorf("'-light-mode' is imcompatible with `-monitor-block-sizes`")
}
if len(nodeKeys) > 0 {
return nil, fmt.Errorf("'-light-mode' is imcompatible with `-nodekey`")
}
if len(balanceAddresses) > 0 {
return nil, fmt.Errorf("'-light-mode' is imcompatible with `-balance-addresses`")
}
} }
// get votekeys from rpc: // get votekeys from rpc:

View File

@ -44,7 +44,7 @@ func TestNewExporterConfig(t *testing.T) {
listenAddress: ":8080", listenAddress: ":8080",
nodeKeys: simulator.Nodekeys, nodeKeys: simulator.Nodekeys,
balanceAddresses: []string{"xxx", "yyy", "zzz"}, balanceAddresses: []string{"xxx", "yyy", "zzz"},
comprehensiveSlotTracking: true, comprehensiveSlotTracking: false,
monitorBlockSizes: false, monitorBlockSizes: false,
lightMode: true, lightMode: true,
slotPace: time.Second, slotPace: time.Second,

View File

@ -55,7 +55,7 @@ func NewSolanaCollector(client *rpc.Client, config *ExporterConfig) *SolanaColle
config: config, config: config,
ValidatorActiveStake: NewGaugeDesc( ValidatorActiveStake: NewGaugeDesc(
"solana_validator_active_stake", "solana_validator_active_stake",
fmt.Sprintf("Active stake per validator (represented by %s and %s)", VotekeyLabel, NodekeyLabel), fmt.Sprintf("Active stake (in SOL) per validator (represented by %s and %s)", VotekeyLabel, NodekeyLabel),
VotekeyLabel, NodekeyLabel, VotekeyLabel, NodekeyLabel,
), ),
ValidatorLastVote: NewGaugeDesc( ValidatorLastVote: NewGaugeDesc(
@ -134,7 +134,7 @@ func (c *SolanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro
for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) { for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) {
accounts := []string{account.VotePubkey, account.NodePubkey} accounts := []string{account.VotePubkey, account.NodePubkey}
ch <- c.ValidatorActiveStake.MustNewConstMetric(float64(account.ActivatedStake), accounts...) ch <- c.ValidatorActiveStake.MustNewConstMetric(float64(account.ActivatedStake)/rpc.LamportsInSol, accounts...)
ch <- c.ValidatorLastVote.MustNewConstMetric(float64(account.LastVote), accounts...) ch <- c.ValidatorLastVote.MustNewConstMetric(float64(account.LastVote), accounts...)
ch <- c.ValidatorRootSlot.MustNewConstMetric(float64(account.RootSlot), accounts...) ch <- c.ValidatorRootSlot.MustNewConstMetric(float64(account.RootSlot), accounts...)
} }

View File

@ -210,11 +210,13 @@ func TestSolanaCollector(t *testing.T) {
collector := NewSolanaCollector(client, newTestConfig(simulator, false)) collector := NewSolanaCollector(client, newTestConfig(simulator, false))
prometheus.NewPedanticRegistry().MustRegister(collector) prometheus.NewPedanticRegistry().MustRegister(collector)
stake := float64(1_000_000) / rpc.LamportsInSol
testCases := []collectionTest{ testCases := []collectionTest{
collector.ValidatorActiveStake.makeCollectionTest( collector.ValidatorActiveStake.makeCollectionTest(
NewLV(1_000_000, "aaa", "AAA"), NewLV(stake, "aaa", "AAA"),
NewLV(1_000_000, "bbb", "BBB"), NewLV(stake, "bbb", "BBB"),
NewLV(1_000_000, "ccc", "CCC"), NewLV(stake, "ccc", "CCC"),
), ),
collector.ValidatorLastVote.makeCollectionTest( collector.ValidatorLastVote.makeCollectionTest(
NewLV(34, "aaa", "AAA"), NewLV(34, "aaa", "AAA"),

View File

@ -39,7 +39,7 @@ type SlotWatcher struct {
EpochLastSlotMetric prometheus.Gauge EpochLastSlotMetric prometheus.Gauge
LeaderSlotsMetric *prometheus.CounterVec LeaderSlotsMetric *prometheus.CounterVec
LeaderSlotsByEpochMetric *prometheus.CounterVec LeaderSlotsByEpochMetric *prometheus.CounterVec
InflationRewardsMetric *prometheus.GaugeVec InflationRewardsMetric *prometheus.CounterVec
FeeRewardsMetric *prometheus.CounterVec FeeRewardsMetric *prometheus.CounterVec
BlockSizeMetric *prometheus.GaugeVec BlockSizeMetric *prometheus.GaugeVec
BlockHeightMetric prometheus.Gauge BlockHeightMetric prometheus.Gauge
@ -53,28 +53,30 @@ func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
config: config, config: config,
// metrics: // metrics:
TotalTransactionsMetric: prometheus.NewGauge(prometheus.GaugeOpts{ TotalTransactionsMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "solana_total_transactions", // even though this isn't a counter, it is supposed to act as one,
// and so we name it with the _total suffix
Name: "solana_node_transactions_total",
Help: "Total number of transactions processed without error since genesis.", Help: "Total number of transactions processed without error since genesis.",
}), }),
SlotHeightMetric: prometheus.NewGauge(prometheus.GaugeOpts{ SlotHeightMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "solana_slot_height", Name: "solana_node_slot_height",
Help: "The current slot number", Help: "The current slot number",
}), }),
EpochNumberMetric: prometheus.NewGauge(prometheus.GaugeOpts{ EpochNumberMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "solana_epoch_number", Name: "solana_node_epoch_number",
Help: "The current epoch number.", Help: "The current epoch number.",
}), }),
EpochFirstSlotMetric: prometheus.NewGauge(prometheus.GaugeOpts{ EpochFirstSlotMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "solana_epoch_first_slot", Name: "solana_node_epoch_first_slot",
Help: "Current epoch's first slot [inclusive].", Help: "Current epoch's first slot [inclusive].",
}), }),
EpochLastSlotMetric: prometheus.NewGauge(prometheus.GaugeOpts{ EpochLastSlotMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "solana_epoch_last_slot", Name: "solana_node_epoch_last_slot",
Help: "Current epoch's last slot [inclusive].", Help: "Current epoch's last slot [inclusive].",
}), }),
LeaderSlotsMetric: prometheus.NewCounterVec( LeaderSlotsMetric: prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "solana_leader_slots", Name: "solana_validator_leader_slots_total",
Help: fmt.Sprintf( Help: fmt.Sprintf(
"Number of slots processed, grouped by %s, and %s ('%s' or '%s')", "Number of slots processed, grouped by %s, and %s ('%s' or '%s')",
NodekeyLabel, SkipStatusLabel, StatusValid, StatusSkipped, NodekeyLabel, SkipStatusLabel, StatusValid, StatusSkipped,
@ -84,7 +86,7 @@ func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
), ),
LeaderSlotsByEpochMetric: prometheus.NewCounterVec( LeaderSlotsByEpochMetric: prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "solana_leader_slots_by_epoch", Name: "solana_validator_leader_slots_by_epoch_total",
Help: fmt.Sprintf( Help: fmt.Sprintf(
"Number of slots processed, grouped by %s, %s ('%s' or '%s'), and %s", "Number of slots processed, grouped by %s, %s ('%s' or '%s'), and %s",
NodekeyLabel, SkipStatusLabel, StatusValid, StatusSkipped, EpochLabel, NodekeyLabel, SkipStatusLabel, StatusValid, StatusSkipped, EpochLabel,
@ -92,29 +94,29 @@ func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
}, },
[]string{NodekeyLabel, EpochLabel, SkipStatusLabel}, []string{NodekeyLabel, EpochLabel, SkipStatusLabel},
), ),
InflationRewardsMetric: prometheus.NewGaugeVec( InflationRewardsMetric: prometheus.NewCounterVec(
prometheus.GaugeOpts{ prometheus.CounterOpts{
Name: "solana_inflation_rewards", Name: "solana_validator_inflation_rewards_total",
Help: fmt.Sprintf("Inflation reward earned, grouped by %s and %s", VotekeyLabel, EpochLabel), Help: fmt.Sprintf("Inflation reward earned, grouped by %s and %s", VotekeyLabel, EpochLabel),
}, },
[]string{VotekeyLabel, EpochLabel}, []string{VotekeyLabel, EpochLabel},
), ),
FeeRewardsMetric: prometheus.NewCounterVec( FeeRewardsMetric: prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "solana_fee_rewards", Name: "solana_validator_fee_rewards_total",
Help: fmt.Sprintf("Transaction fee rewards earned, grouped by %s and %s", NodekeyLabel, EpochLabel), Help: fmt.Sprintf("Transaction fee rewards earned, grouped by %s and %s", NodekeyLabel, EpochLabel),
}, },
[]string{NodekeyLabel, EpochLabel}, []string{NodekeyLabel, EpochLabel},
), ),
BlockSizeMetric: prometheus.NewGaugeVec( BlockSizeMetric: prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "solana_block_size", Name: "solana_validator_block_size",
Help: fmt.Sprintf("Number of transactions per block, grouped by %s", NodekeyLabel), Help: fmt.Sprintf("Number of transactions per block, grouped by %s", NodekeyLabel),
}, },
[]string{NodekeyLabel, TransactionTypeLabel}, []string{NodekeyLabel, TransactionTypeLabel},
), ),
BlockHeightMetric: prometheus.NewGauge(prometheus.GaugeOpts{ BlockHeightMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "solana_block_height", Name: "solana_node_block_height",
Help: "The current block height of the node", Help: "The current block height of the node",
}), }),
} }
@ -174,6 +176,7 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context) {
c.trackEpoch(ctx, epochInfo) c.trackEpoch(ctx, epochInfo)
} }
c.logger.Infof("Current slot: %v", epochInfo.AbsoluteSlot)
c.TotalTransactionsMetric.Set(float64(epochInfo.TransactionCount)) c.TotalTransactionsMetric.Set(float64(epochInfo.TransactionCount))
c.SlotHeightMetric.Set(float64(epochInfo.AbsoluteSlot)) c.SlotHeightMetric.Set(float64(epochInfo.AbsoluteSlot))
c.BlockHeightMetric.Set(float64(epochInfo.BlockHeight)) c.BlockHeightMetric.Set(float64(epochInfo.BlockHeight))
@ -281,21 +284,21 @@ func (c *SlotWatcher) checkValidSlotRange(from, to int64) error {
// moveSlotWatermark performs all the slot-watching tasks required to move the slotWatermark to the provided 'to' slot. // moveSlotWatermark performs all the slot-watching tasks required to move the slotWatermark to the provided 'to' slot.
func (c *SlotWatcher) moveSlotWatermark(ctx context.Context, to int64) { func (c *SlotWatcher) moveSlotWatermark(ctx context.Context, to int64) {
c.fetchAndEmitBlockProduction(ctx, to) c.logger.Infof("Moving watermark %v -> %v", c.slotWatermark, to)
c.fetchAndEmitBlockInfos(ctx, to) startSlot := c.slotWatermark + 1
c.fetchAndEmitBlockProduction(ctx, startSlot, to)
c.fetchAndEmitBlockInfos(ctx, startSlot, to)
c.slotWatermark = to c.slotWatermark = to
} }
// fetchAndEmitBlockProduction fetches block production up to the provided endSlot, emits the prometheus metrics, // fetchAndEmitBlockProduction fetches block production from startSlot up to the provided endSlot [inclusive],
// and updates the SlotWatcher.slotWatermark accordingly // and emits the prometheus metrics,
func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, endSlot int64) { func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, startSlot, endSlot int64) {
if c.config.LightMode { if c.config.LightMode {
c.logger.Debug("Skipping block-production fetching in light mode.") c.logger.Debug("Skipping block-production fetching in light mode.")
return return
} }
// add 1 because GetBlockProduction's range is inclusive, and the watermark is already tracked c.logger.Debugf("Fetching block production in [%v -> %v]", startSlot, endSlot)
startSlot := c.slotWatermark + 1
c.logger.Infof("Fetching block production in [%v -> %v]", startSlot, endSlot)
// make sure the bounds are contained within the epoch we are currently watching: // make sure the bounds are contained within the epoch we are currently watching:
if err := c.checkValidSlotRange(startSlot, endSlot); err != nil { if err := c.checkValidSlotRange(startSlot, endSlot); err != nil {
@ -324,18 +327,17 @@ func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, endSlot i
} }
} }
c.logger.Infof("Fetched block production in [%v -> %v]", startSlot, endSlot) c.logger.Debugf("Fetched block production in [%v -> %v]", startSlot, endSlot)
} }
// fetchAndEmitBlockInfos fetches and emits all the fee rewards (+ block sizes) for the tracked addresses between the // fetchAndEmitBlockInfos fetches and emits all the fee rewards (+ block sizes) for the tracked addresses between the
// slotWatermark and endSlot // startSlot and endSlot [inclusive]
func (c *SlotWatcher) fetchAndEmitBlockInfos(ctx context.Context, endSlot int64) { func (c *SlotWatcher) fetchAndEmitBlockInfos(ctx context.Context, startSlot, endSlot int64) {
if c.config.LightMode { if c.config.LightMode {
c.logger.Debug("Skipping block-infos fetching in light mode.") c.logger.Debug("Skipping block-infos fetching in light mode.")
return return
} }
startSlot := c.slotWatermark + 1 c.logger.Debugf("Fetching fee rewards in [%v -> %v]", startSlot, endSlot)
c.logger.Infof("Fetching fee rewards in [%v -> %v]", startSlot, endSlot)
if err := c.checkValidSlotRange(startSlot, endSlot); err != nil { if err := c.checkValidSlotRange(startSlot, endSlot); err != nil {
c.logger.Fatalf("invalid slot range: %v", err) c.logger.Fatalf("invalid slot range: %v", err)
@ -355,7 +357,7 @@ func (c *SlotWatcher) fetchAndEmitBlockInfos(ctx context.Context, endSlot int64)
} }
} }
c.logger.Infof("Fetched fee rewards in [%v -> %v]", startSlot, endSlot) c.logger.Debugf("Fetched fee rewards in [%v -> %v]", startSlot, endSlot)
} }
// fetchAndEmitSingleBlockInfo fetches and emits the fee reward + block size for a single block. // fetchAndEmitSingleBlockInfo fetches and emits the fee reward + block size for a single block.
@ -423,7 +425,7 @@ func (c *SlotWatcher) fetchAndEmitInflationRewards(ctx context.Context, epoch in
for i, rewardInfo := range rewardInfos { for i, rewardInfo := range rewardInfos {
address := c.config.VoteKeys[i] address := c.config.VoteKeys[i]
reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol) reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol)
c.InflationRewardsMetric.WithLabelValues(address, toString(epoch)).Set(reward) c.InflationRewardsMetric.WithLabelValues(address, toString(epoch)).Add(reward)
} }
c.logger.Infof("Fetched inflation reward for epoch %v.", epoch) c.logger.Infof("Fetched inflation reward for epoch %v.", epoch)
return nil return nil

View File

@ -109,38 +109,38 @@ func TestSlotWatcher_WatchSlots_Static(t *testing.T) {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
type testCase struct { type testCase struct {
name string
expectedValue float64 expectedValue float64
metric prometheus.Gauge metric prometheus.Collector
} }
// epoch info tests: // epoch info tests:
firstSlot, lastSlot := GetEpochBounds(epochInfo) firstSlot, lastSlot := GetEpochBounds(epochInfo)
tests := []testCase{ tests := []testCase{
{expectedValue: float64(epochInfo.AbsoluteSlot), metric: watcher.SlotHeightMetric}, {"slot_height", float64(epochInfo.AbsoluteSlot), watcher.SlotHeightMetric},
{expectedValue: float64(epochInfo.TransactionCount), metric: watcher.TotalTransactionsMetric}, {"total_transactions", float64(epochInfo.TransactionCount), watcher.TotalTransactionsMetric},
{expectedValue: float64(epochInfo.Epoch), metric: watcher.EpochNumberMetric}, {"epoch_number", float64(epochInfo.Epoch), watcher.EpochNumberMetric},
{expectedValue: float64(firstSlot), metric: watcher.EpochFirstSlotMetric}, {"epoch_first_slot", float64(firstSlot), watcher.EpochFirstSlotMetric},
{expectedValue: float64(lastSlot), metric: watcher.EpochLastSlotMetric}, {"epoch_last_slot", float64(lastSlot), watcher.EpochLastSlotMetric},
} }
// add inflation reward tests: // add inflation reward tests:
inflationRewards, err := client.GetInflationReward(ctx, rpc.CommitmentFinalized, simulator.Votekeys, 2) inflationRewards, err := client.GetInflationReward(ctx, rpc.CommitmentFinalized, simulator.Votekeys, 2)
assert.NoError(t, err) assert.NoError(t, err)
for i, rewardInfo := range inflationRewards { for i, rewardInfo := range inflationRewards {
epoch := fmt.Sprintf("%v", epochInfo.Epoch)
tests = append( tests = append(
tests, tests,
testCase{ testCase{
expectedValue: float64(rewardInfo.Amount) / float64(rpc.LamportsInSol), fmt.Sprintf("inflation_rewards_%s", simulator.Votekeys[i]),
metric: watcher.InflationRewardsMetric.WithLabelValues(simulator.Votekeys[i], epoch), float64(rewardInfo.Amount) / float64(rpc.LamportsInSol),
watcher.InflationRewardsMetric.WithLabelValues(simulator.Votekeys[i], toString(epochInfo.Epoch)),
}, },
) )
} }
for _, testCase := range tests { for _, test := range tests {
name := extractName(testCase.metric.Desc()) t.Run(test.name, func(t *testing.T) {
t.Run(name, func(t *testing.T) { assert.Equal(t, test.expectedValue, testutil.ToFloat64(test.metric))
assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric))
}) })
} }
} }