Merge pull request #72 from asymmetric-research/bug-fix
Running fixes/updates
This commit is contained in:
commit
bb45ab3e76
47
README.md
47
README.md
|
@ -61,29 +61,30 @@ The exporter is configured via the following command line arguments:
|
||||||
|
|
||||||
The table below describes all the metrics collected by the `solana-exporter`:
|
The table below describes all the metrics collected by the `solana-exporter`:
|
||||||
|
|
||||||
| Metric | Description | Labels |
|
| Metric | Description | Labels |
|
||||||
|-------------------------------------|------------------------------------------------------------------------------------------|-------------------------------|
|
|------------------------------------------------|------------------------------------------------------------------------------------------|-------------------------------|
|
||||||
| `solana_validator_active_stake` | Active stake per validator. | `votekey`, `nodekey` |
|
| `solana_validator_active_stake` | Active stake per validator. | `votekey`, `nodekey` |
|
||||||
| `solana_validator_last_vote` | Last voted-on slot per validator. | `votekey`, `nodekey` |
|
| `solana_validator_last_vote` | Last voted-on slot per validator. | `votekey`, `nodekey` |
|
||||||
| `solana_validator_root_slot` | Root slot per validator. | `votekey`, `nodekey` |
|
| `solana_validator_root_slot` | Root slot per validator. | `votekey`, `nodekey` |
|
||||||
| `solana_validator_delinquent` | Whether a validator is delinquent. | `votekey`, `nodekey` |
|
| `solana_validator_delinquent` | Whether a validator is delinquent. | `votekey`, `nodekey` |
|
||||||
| `solana_account_balance` | Solana account balances. | `address` |
|
| `solana_account_balance` | Solana account balances. | `address` |
|
||||||
| `solana_node_version` | Node version of solana.* | `version` |
|
| `solana_node_version` | Node version of solana.* | `version` |
|
||||||
| `solana_node_is_healthy` | Whether the node is healthy.* | N/A |
|
| `solana_node_is_healthy` | Whether the node is healthy.* | 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_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_node_total_transactions` | Total number of transactions processed without error since genesis.* | N/A |
|
| `solana_node_transactions_total` | Total number of transactions processed without error since genesis.* | N/A |
|
||||||
| `solana_node_slot_height` | The current slot number.* | N/A |
|
| `solana_node_slot_height` | The current slot number.* | N/A |
|
||||||
| `solana_node_epoch_number` | The current epoch number.* | N/A |
|
| `solana_node_epoch_number` | The current epoch number.* | N/A |
|
||||||
| `solana_node_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_node_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_node_leader_slots` | Number of slots processed. | `status`, `nodekey` |
|
| `solana_validator_leader_slots_total` | Number of slots processed. | `status`, `nodekey` |
|
||||||
| `solana_node_leader_slots_by_epoch` | Number of slots processed. | `status`, `nodekey`, `epoch` |
|
| `solana_validator_leader_slots_by_epoch_total` | Number of slots processed per validator. | `status`, `nodekey`, `epoch` |
|
||||||
| `solana_node_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` |
|
| `solana_cluster_slots_by_epoch_total` | Number of slots processed by the cluster. | `status`, `epoch` |
|
||||||
| `solana_node_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` |
|
| `solana_validator_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` |
|
||||||
| `solana_node_block_size` | Number of transactions per block. | `nodekey`, `transaction_type` |
|
| `solana_validator_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` |
|
||||||
| `solana_node_block_height` | The current block height of the node.* | N/A |
|
| `solana_validator_block_size` | Number of transactions per block. | `nodekey`, `transaction_type` |
|
||||||
|
| `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`.
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,6 @@ const (
|
||||||
StatusSkipped = "skipped"
|
StatusSkipped = "skipped"
|
||||||
StatusValid = "valid"
|
StatusValid = "valid"
|
||||||
|
|
||||||
StateCurrent = "current"
|
|
||||||
StateDelinquent = "delinquent"
|
|
||||||
|
|
||||||
TransactionTypeVote = "vote"
|
TransactionTypeVote = "vote"
|
||||||
TransactionTypeNonVote = "non_vote"
|
TransactionTypeNonVote = "non_vote"
|
||||||
)
|
)
|
|
@ -32,17 +32,18 @@ type SlotWatcher struct {
|
||||||
leaderSchedule map[string][]int64
|
leaderSchedule map[string][]int64
|
||||||
|
|
||||||
// prometheus:
|
// prometheus:
|
||||||
TotalTransactionsMetric prometheus.Gauge
|
TotalTransactionsMetric prometheus.Gauge
|
||||||
SlotHeightMetric prometheus.Gauge
|
SlotHeightMetric prometheus.Gauge
|
||||||
EpochNumberMetric prometheus.Gauge
|
EpochNumberMetric prometheus.Gauge
|
||||||
EpochFirstSlotMetric prometheus.Gauge
|
EpochFirstSlotMetric prometheus.Gauge
|
||||||
EpochLastSlotMetric prometheus.Gauge
|
EpochLastSlotMetric prometheus.Gauge
|
||||||
LeaderSlotsMetric *prometheus.CounterVec
|
LeaderSlotsMetric *prometheus.CounterVec
|
||||||
LeaderSlotsByEpochMetric *prometheus.CounterVec
|
LeaderSlotsByEpochMetric *prometheus.CounterVec
|
||||||
InflationRewardsMetric *prometheus.CounterVec
|
ClusterSlotsByEpochMetric *prometheus.CounterVec
|
||||||
FeeRewardsMetric *prometheus.CounterVec
|
InflationRewardsMetric *prometheus.CounterVec
|
||||||
BlockSizeMetric *prometheus.GaugeVec
|
FeeRewardsMetric *prometheus.CounterVec
|
||||||
BlockHeightMetric prometheus.Gauge
|
BlockSizeMetric *prometheus.GaugeVec
|
||||||
|
BlockHeightMetric prometheus.Gauge
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
|
func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
|
||||||
|
@ -94,6 +95,16 @@ func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
|
||||||
},
|
},
|
||||||
[]string{NodekeyLabel, EpochLabel, SkipStatusLabel},
|
[]string{NodekeyLabel, EpochLabel, SkipStatusLabel},
|
||||||
),
|
),
|
||||||
|
ClusterSlotsByEpochMetric: prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "solana_cluster_slots_by_epoch_total",
|
||||||
|
Help: fmt.Sprintf(
|
||||||
|
"Number of slots processed by the cluster, grouped by %s ('%s' or '%s'), and %s",
|
||||||
|
SkipStatusLabel, StatusValid, StatusSkipped, EpochLabel,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[]string{EpochLabel, SkipStatusLabel},
|
||||||
|
),
|
||||||
InflationRewardsMetric: prometheus.NewCounterVec(
|
InflationRewardsMetric: prometheus.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "solana_validator_inflation_rewards_total",
|
Name: "solana_validator_inflation_rewards_total",
|
||||||
|
@ -320,11 +331,15 @@ func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, startSlot
|
||||||
c.LeaderSlotsMetric.WithLabelValues(address, StatusValid).Add(valid)
|
c.LeaderSlotsMetric.WithLabelValues(address, StatusValid).Add(valid)
|
||||||
c.LeaderSlotsMetric.WithLabelValues(address, StatusSkipped).Add(skipped)
|
c.LeaderSlotsMetric.WithLabelValues(address, StatusSkipped).Add(skipped)
|
||||||
|
|
||||||
|
epochStr := toString(c.currentEpoch)
|
||||||
if slices.Contains(c.config.NodeKeys, address) || c.config.ComprehensiveSlotTracking {
|
if slices.Contains(c.config.NodeKeys, address) || c.config.ComprehensiveSlotTracking {
|
||||||
epochStr := toString(c.currentEpoch)
|
|
||||||
c.LeaderSlotsByEpochMetric.WithLabelValues(address, epochStr, StatusValid).Add(valid)
|
c.LeaderSlotsByEpochMetric.WithLabelValues(address, epochStr, StatusValid).Add(valid)
|
||||||
c.LeaderSlotsByEpochMetric.WithLabelValues(address, epochStr, StatusSkipped).Add(skipped)
|
c.LeaderSlotsByEpochMetric.WithLabelValues(address, epochStr, StatusSkipped).Add(skipped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// additionally, track block production for the whole cluster:
|
||||||
|
c.ClusterSlotsByEpochMetric.WithLabelValues(epochStr, StatusValid).Add(valid)
|
||||||
|
c.ClusterSlotsByEpochMetric.WithLabelValues(epochStr, StatusSkipped).Add(skipped)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Debugf("Fetched block production in [%v -> %v]", startSlot, endSlot)
|
c.logger.Debugf("Fetched block production in [%v -> %v]", startSlot, endSlot)
|
||||||
|
@ -381,8 +396,9 @@ func (c *SlotWatcher) fetchAndEmitSingleBlockInfo(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foundFeeReward := false
|
||||||
for _, reward := range block.Rewards {
|
for _, reward := range block.Rewards {
|
||||||
if reward.RewardType == "fee" {
|
if strings.ToLower(reward.RewardType) == "fee" {
|
||||||
// make sure we haven't made a logic issue or something:
|
// make sure we haven't made a logic issue or something:
|
||||||
assertf(
|
assertf(
|
||||||
reward.Pubkey == nodekey,
|
reward.Pubkey == nodekey,
|
||||||
|
@ -390,11 +406,16 @@ func (c *SlotWatcher) fetchAndEmitSingleBlockInfo(
|
||||||
nodekey,
|
nodekey,
|
||||||
reward.Pubkey,
|
reward.Pubkey,
|
||||||
)
|
)
|
||||||
amount := float64(reward.Lamports) / float64(rpc.LamportsInSol)
|
amount := float64(reward.Lamports) / rpc.LamportsInSol
|
||||||
c.FeeRewardsMetric.WithLabelValues(nodekey, toString(epoch)).Add(amount)
|
c.FeeRewardsMetric.WithLabelValues(nodekey, toString(epoch)).Add(amount)
|
||||||
|
foundFeeReward = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !foundFeeReward {
|
||||||
|
c.logger.Errorf("No fee reward for slot %d", slot)
|
||||||
|
}
|
||||||
|
|
||||||
// track block size:
|
// track block size:
|
||||||
if c.config.MonitorBlockSizes {
|
if c.config.MonitorBlockSizes {
|
||||||
// now count and emit votes:
|
// now count and emit votes:
|
||||||
|
@ -424,7 +445,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) / rpc.LamportsInSol
|
||||||
c.InflationRewardsMetric.WithLabelValues(address, toString(epoch)).Add(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)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"regexp"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -21,22 +20,6 @@ type slotMetricValues struct {
|
||||||
BlockHeight float64
|
BlockHeight float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSlotMetricValues(watcher *SlotWatcher) slotMetricValues {
|
func getSlotMetricValues(watcher *SlotWatcher) slotMetricValues {
|
||||||
return slotMetricValues{
|
return slotMetricValues{
|
||||||
SlotHeight: testutil.ToFloat64(watcher.SlotHeightMetric),
|
SlotHeight: testutil.ToFloat64(watcher.SlotHeightMetric),
|
||||||
|
@ -217,25 +200,40 @@ func TestSlotWatcher_WatchSlots_Dynamic(t *testing.T) {
|
||||||
if final.EpochNumber > initial.EpochNumber {
|
if final.EpochNumber > initial.EpochNumber {
|
||||||
epochChanged = true
|
epochChanged = true
|
||||||
|
|
||||||
// run some tests for the previous epoch:
|
// run some tests for the previous epoch, starting with cluster as a whole:
|
||||||
|
epochStr := toString(initial.EpochNumber)
|
||||||
|
assert.Equalf(t,
|
||||||
|
float64(simulator.EpochSize*3/4),
|
||||||
|
testutil.ToFloat64(watcher.ClusterSlotsByEpochMetric.WithLabelValues(epochStr, StatusValid)),
|
||||||
|
"Incorrect %s cluster slots at epoch %s",
|
||||||
|
StatusValid, epochStr,
|
||||||
|
)
|
||||||
|
assert.Equalf(t,
|
||||||
|
float64(simulator.EpochSize*1/4),
|
||||||
|
testutil.ToFloat64(watcher.ClusterSlotsByEpochMetric.WithLabelValues(epochStr, StatusSkipped)),
|
||||||
|
"Incorrect %s cluster slots at epoch %s",
|
||||||
|
StatusSkipped, epochStr,
|
||||||
|
)
|
||||||
|
|
||||||
|
// now test per validator:
|
||||||
leaderSlotsPerEpoch := simulator.EpochSize / len(simulator.Nodekeys)
|
leaderSlotsPerEpoch := simulator.EpochSize / len(simulator.Nodekeys)
|
||||||
for i, nodekey := range simulator.Nodekeys {
|
for i, nodekey := range simulator.Nodekeys {
|
||||||
// leader slots per epoch:
|
// leader slots per epoch:
|
||||||
assert.Equalf(t,
|
assert.Equalf(t,
|
||||||
float64(leaderSlotsPerEpoch*3/4),
|
float64(leaderSlotsPerEpoch*3/4),
|
||||||
testutil.ToFloat64(
|
testutil.ToFloat64(
|
||||||
watcher.LeaderSlotsByEpochMetric.WithLabelValues(nodekey, toString(initial.EpochNumber), StatusValid),
|
watcher.LeaderSlotsByEpochMetric.WithLabelValues(nodekey, epochStr, StatusValid),
|
||||||
),
|
),
|
||||||
"Incorrect %s leader slots for %s at epoch %v",
|
"Incorrect %s leader slots for %s at epoch %s",
|
||||||
StatusValid, nodekey, initial.EpochNumber,
|
StatusValid, nodekey, epochStr,
|
||||||
)
|
)
|
||||||
assert.Equalf(t,
|
assert.Equalf(t,
|
||||||
float64(leaderSlotsPerEpoch*1/4),
|
float64(leaderSlotsPerEpoch*1/4),
|
||||||
testutil.ToFloat64(
|
testutil.ToFloat64(
|
||||||
watcher.LeaderSlotsByEpochMetric.WithLabelValues(nodekey, toString(initial.EpochNumber), StatusSkipped),
|
watcher.LeaderSlotsByEpochMetric.WithLabelValues(nodekey, epochStr, StatusSkipped),
|
||||||
),
|
),
|
||||||
"Incorrect %s leader slots for %s at epoch %v",
|
"Incorrect %s leader slots for %s at epoch %s",
|
||||||
StatusSkipped, nodekey, initial.EpochNumber,
|
StatusSkipped, nodekey, epochStr,
|
||||||
)
|
)
|
||||||
|
|
||||||
// inflation rewards:
|
// inflation rewards:
|
||||||
|
@ -243,20 +241,20 @@ func TestSlotWatcher_WatchSlots_Dynamic(t *testing.T) {
|
||||||
assert.Equalf(t,
|
assert.Equalf(t,
|
||||||
float64(simulator.InflationRewardLamports)/rpc.LamportsInSol,
|
float64(simulator.InflationRewardLamports)/rpc.LamportsInSol,
|
||||||
testutil.ToFloat64(
|
testutil.ToFloat64(
|
||||||
watcher.InflationRewardsMetric.WithLabelValues(votekey, toString(initial.EpochNumber)),
|
watcher.InflationRewardsMetric.WithLabelValues(votekey, epochStr),
|
||||||
),
|
),
|
||||||
"Incorrect inflation reward for %s at epoch %v",
|
"Incorrect inflation reward for %s at epoch %s",
|
||||||
votekey, initial.EpochNumber,
|
votekey, epochStr,
|
||||||
)
|
)
|
||||||
|
|
||||||
// fee rewards:
|
// fee rewards:
|
||||||
assert.Equalf(t,
|
assert.Equalf(t,
|
||||||
float64(simulator.FeeRewardLamports*leaderSlotsPerEpoch*3/4)/rpc.LamportsInSol,
|
float64(simulator.FeeRewardLamports*leaderSlotsPerEpoch*3/4)/rpc.LamportsInSol,
|
||||||
testutil.ToFloat64(
|
testutil.ToFloat64(
|
||||||
watcher.FeeRewardsMetric.WithLabelValues(nodekey, toString(initial.EpochNumber)),
|
watcher.FeeRewardsMetric.WithLabelValues(nodekey, epochStr),
|
||||||
),
|
),
|
||||||
"Incorrect fee reward for %s at epoch %v",
|
"Incorrect fee reward for %s at epoch %s",
|
||||||
nodekey, initial.EpochNumber,
|
nodekey, epochStr,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -31,10 +31,6 @@ type (
|
||||||
Commitment string
|
Commitment string
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c Commitment) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]string{"commitment": string(c)})
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// LamportsInSol is the number of lamports in 1 SOL (a billion)
|
// LamportsInSol is the number of lamports in 1 SOL (a billion)
|
||||||
LamportsInSol = 1_000_000_000
|
LamportsInSol = 1_000_000_000
|
||||||
|
@ -85,7 +81,7 @@ func getResponse[T any](
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error processing %s rpc call: %w", method, err)
|
return fmt.Errorf("error processing %s rpc call: %w", method, err)
|
||||||
}
|
}
|
||||||
// log response:
|
// debug log response:
|
||||||
logger.Debugf("%s response: %v", method, string(body))
|
logger.Debugf("%s response: %v", method, string(body))
|
||||||
|
|
||||||
// unmarshal the response into the predicted format
|
// unmarshal the response into the predicted format
|
||||||
|
@ -105,7 +101,8 @@ func getResponse[T any](
|
||||||
// See API docs: https://solana.com/docs/rpc/http/getepochinfo
|
// See API docs: https://solana.com/docs/rpc/http/getepochinfo
|
||||||
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
|
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
|
||||||
var resp Response[EpochInfo]
|
var resp Response[EpochInfo]
|
||||||
if err := getResponse(ctx, c, "getEpochInfo", []any{commitment}, &resp); err != nil {
|
config := map[string]string{"commitment": string(commitment)}
|
||||||
|
if err := getResponse(ctx, c, "getEpochInfo", []any{config}, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resp.Result, nil
|
return &resp.Result, nil
|
||||||
|
|
Loading…
Reference in New Issue