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`:
|
||||
|
||||
| Metric | Description | Labels |
|
||||
|-------------------------------------|------------------------------------------------------------------------------------------|-------------------------------|
|
||||
| `solana_validator_active_stake` | Active stake 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_delinquent` | Whether a validator is delinquent. | `votekey`, `nodekey` |
|
||||
| `solana_account_balance` | Solana account balances. | `address` |
|
||||
| `solana_node_version` | Node version of solana.* | `version` |
|
||||
| `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_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_total_transactions` | Total number of transactions processed without error since genesis.* | 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_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_leader_slots` | Number of slots processed. | `status`, `nodekey` |
|
||||
| `solana_node_leader_slots_by_epoch` | Number of slots processed. | `status`, `nodekey`, `epoch` |
|
||||
| `solana_node_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` |
|
||||
| `solana_node_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` |
|
||||
| `solana_node_block_size` | Number of transactions per block. | `nodekey`, `transaction_type` |
|
||||
| `solana_node_block_height` | The current block height of the node.* | N/A |
|
||||
| Metric | Description | Labels |
|
||||
|------------------------------------------------|------------------------------------------------------------------------------------------|-------------------------------|
|
||||
| `solana_validator_active_stake` | Active stake 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_delinquent` | Whether a validator is delinquent. | `votekey`, `nodekey` |
|
||||
| `solana_account_balance` | Solana account balances. | `address` |
|
||||
| `solana_node_version` | Node version of solana.* | `version` |
|
||||
| `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_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_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_epoch_number` | The current epoch number.* | 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_validator_leader_slots_total` | Number of slots processed. | `status`, `nodekey` |
|
||||
| `solana_validator_leader_slots_by_epoch_total` | Number of slots processed per validator. | `status`, `nodekey`, `epoch` |
|
||||
| `solana_cluster_slots_by_epoch_total` | Number of slots processed by the cluster. | `status`, `epoch` |
|
||||
| `solana_validator_inflation_rewards` | Inflation reward earned. | `votekey`, `epoch` |
|
||||
| `solana_validator_fee_rewards` | Transaction fee rewards earned. | `nodekey`, `epoch` |
|
||||
| `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`.
|
||||
|
||||
|
|
|
@ -22,9 +22,6 @@ const (
|
|||
StatusSkipped = "skipped"
|
||||
StatusValid = "valid"
|
||||
|
||||
StateCurrent = "current"
|
||||
StateDelinquent = "delinquent"
|
||||
|
||||
TransactionTypeVote = "vote"
|
||||
TransactionTypeNonVote = "non_vote"
|
||||
)
|
|
@ -32,17 +32,18 @@ type SlotWatcher struct {
|
|||
leaderSchedule map[string][]int64
|
||||
|
||||
// prometheus:
|
||||
TotalTransactionsMetric prometheus.Gauge
|
||||
SlotHeightMetric prometheus.Gauge
|
||||
EpochNumberMetric prometheus.Gauge
|
||||
EpochFirstSlotMetric prometheus.Gauge
|
||||
EpochLastSlotMetric prometheus.Gauge
|
||||
LeaderSlotsMetric *prometheus.CounterVec
|
||||
LeaderSlotsByEpochMetric *prometheus.CounterVec
|
||||
InflationRewardsMetric *prometheus.CounterVec
|
||||
FeeRewardsMetric *prometheus.CounterVec
|
||||
BlockSizeMetric *prometheus.GaugeVec
|
||||
BlockHeightMetric prometheus.Gauge
|
||||
TotalTransactionsMetric prometheus.Gauge
|
||||
SlotHeightMetric prometheus.Gauge
|
||||
EpochNumberMetric prometheus.Gauge
|
||||
EpochFirstSlotMetric prometheus.Gauge
|
||||
EpochLastSlotMetric prometheus.Gauge
|
||||
LeaderSlotsMetric *prometheus.CounterVec
|
||||
LeaderSlotsByEpochMetric *prometheus.CounterVec
|
||||
ClusterSlotsByEpochMetric *prometheus.CounterVec
|
||||
InflationRewardsMetric *prometheus.CounterVec
|
||||
FeeRewardsMetric *prometheus.CounterVec
|
||||
BlockSizeMetric *prometheus.GaugeVec
|
||||
BlockHeightMetric prometheus.Gauge
|
||||
}
|
||||
|
||||
func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
|
||||
|
@ -94,6 +95,16 @@ func NewSlotWatcher(client *rpc.Client, config *ExporterConfig) *SlotWatcher {
|
|||
},
|
||||
[]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(
|
||||
prometheus.CounterOpts{
|
||||
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, StatusSkipped).Add(skipped)
|
||||
|
||||
epochStr := toString(c.currentEpoch)
|
||||
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, 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)
|
||||
|
@ -381,8 +396,9 @@ func (c *SlotWatcher) fetchAndEmitSingleBlockInfo(
|
|||
return err
|
||||
}
|
||||
|
||||
foundFeeReward := false
|
||||
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:
|
||||
assertf(
|
||||
reward.Pubkey == nodekey,
|
||||
|
@ -390,11 +406,16 @@ func (c *SlotWatcher) fetchAndEmitSingleBlockInfo(
|
|||
nodekey,
|
||||
reward.Pubkey,
|
||||
)
|
||||
amount := float64(reward.Lamports) / float64(rpc.LamportsInSol)
|
||||
amount := float64(reward.Lamports) / rpc.LamportsInSol
|
||||
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:
|
||||
if c.config.MonitorBlockSizes {
|
||||
// now count and emit votes:
|
||||
|
@ -424,7 +445,7 @@ func (c *SlotWatcher) fetchAndEmitInflationRewards(ctx context.Context, epoch in
|
|||
|
||||
for i, rewardInfo := range rewardInfos {
|
||||
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.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/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -21,22 +20,6 @@ type slotMetricValues struct {
|
|||
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 {
|
||||
return slotMetricValues{
|
||||
SlotHeight: testutil.ToFloat64(watcher.SlotHeightMetric),
|
||||
|
@ -217,25 +200,40 @@ func TestSlotWatcher_WatchSlots_Dynamic(t *testing.T) {
|
|||
if final.EpochNumber > initial.EpochNumber {
|
||||
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)
|
||||
for i, nodekey := range simulator.Nodekeys {
|
||||
// leader slots per epoch:
|
||||
assert.Equalf(t,
|
||||
float64(leaderSlotsPerEpoch*3/4),
|
||||
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",
|
||||
StatusValid, nodekey, initial.EpochNumber,
|
||||
"Incorrect %s leader slots for %s at epoch %s",
|
||||
StatusValid, nodekey, epochStr,
|
||||
)
|
||||
assert.Equalf(t,
|
||||
float64(leaderSlotsPerEpoch*1/4),
|
||||
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",
|
||||
StatusSkipped, nodekey, initial.EpochNumber,
|
||||
"Incorrect %s leader slots for %s at epoch %s",
|
||||
StatusSkipped, nodekey, epochStr,
|
||||
)
|
||||
|
||||
// inflation rewards:
|
||||
|
@ -243,20 +241,20 @@ func TestSlotWatcher_WatchSlots_Dynamic(t *testing.T) {
|
|||
assert.Equalf(t,
|
||||
float64(simulator.InflationRewardLamports)/rpc.LamportsInSol,
|
||||
testutil.ToFloat64(
|
||||
watcher.InflationRewardsMetric.WithLabelValues(votekey, toString(initial.EpochNumber)),
|
||||
watcher.InflationRewardsMetric.WithLabelValues(votekey, epochStr),
|
||||
),
|
||||
"Incorrect inflation reward for %s at epoch %v",
|
||||
votekey, initial.EpochNumber,
|
||||
"Incorrect inflation reward for %s at epoch %s",
|
||||
votekey, epochStr,
|
||||
)
|
||||
|
||||
// fee rewards:
|
||||
assert.Equalf(t,
|
||||
float64(simulator.FeeRewardLamports*leaderSlotsPerEpoch*3/4)/rpc.LamportsInSol,
|
||||
testutil.ToFloat64(
|
||||
watcher.FeeRewardsMetric.WithLabelValues(nodekey, toString(initial.EpochNumber)),
|
||||
watcher.FeeRewardsMetric.WithLabelValues(nodekey, epochStr),
|
||||
),
|
||||
"Incorrect fee reward for %s at epoch %v",
|
||||
nodekey, initial.EpochNumber,
|
||||
"Incorrect fee reward for %s at epoch %s",
|
||||
nodekey, epochStr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -31,10 +31,6 @@ type (
|
|||
Commitment string
|
||||
)
|
||||
|
||||
func (c Commitment) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{"commitment": string(c)})
|
||||
}
|
||||
|
||||
const (
|
||||
// LamportsInSol is the number of lamports in 1 SOL (a billion)
|
||||
LamportsInSol = 1_000_000_000
|
||||
|
@ -85,7 +81,7 @@ func getResponse[T any](
|
|||
if err != nil {
|
||||
return fmt.Errorf("error processing %s rpc call: %w", method, err)
|
||||
}
|
||||
// log response:
|
||||
// debug log response:
|
||||
logger.Debugf("%s response: %v", method, string(body))
|
||||
|
||||
// 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
|
||||
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
|
||||
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 &resp.Result, nil
|
||||
|
|
Loading…
Reference in New Issue