Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOM: network fees #728

Merged
merged 1 commit into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cmd/monitoring/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,16 @@ func main() {
)
slotHeightSourceFactory := monitoring.NewSlotHeightSourceFactory(
chainReader,
logger.With(log, "component", "souce-slot-height"),
logger.With(log, "component", "source-slot-height"),
)
networkFeesSourceFactory := monitoring.NewNetworkFeesSourceFactory(
chainReader,
logger.With(log, "component", "source-network-fees"),
)
monitor.NetworkSourceFactories = append(monitor.NetworkSourceFactories,
nodeBalancesSourceFactory,
slotHeightSourceFactory,
networkFeesSourceFactory,
)

// exporter names
Expand Down Expand Up @@ -121,9 +126,14 @@ func main() {
logger.With(log, "component", promExporter),
metrics.NewSlotHeight(logger.With(log, "component", promMetrics)),
)
networkFeesExporterFactory := exporter.NewNetworkFeesFactory(
logger.With(log, "component", promExporter),
metrics.NewNetworkFees(logger.With(log, "component", promMetrics)),
)
monitor.NetworkExporterFactories = append(monitor.NetworkExporterFactories,
nodeBalancesExporterFactory,
slotHeightExporterFactory,
networkFeesExporterFactory,
)

monitor.Run()
Expand Down
16 changes: 16 additions & 0 deletions pkg/monitoring/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ChainReader interface {
GetSignaturesForAddressWithOpts(ctx context.Context, account solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) (out []*rpc.TransactionSignature, err error)
GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (out *rpc.GetTransactionResult, err error)
GetSlot(ctx context.Context) (slot uint64, err error)
GetLatestBlock(ctx context.Context, commitment rpc.CommitmentType) (*rpc.GetBlockResult, error)
}

func NewChainReader(client *rpc.Client) ChainReader {
Expand Down Expand Up @@ -56,3 +57,18 @@ func (c *chainReader) GetTransaction(ctx context.Context, txSig solana.Signature
func (c *chainReader) GetSlot(ctx context.Context) (uint64, error) {
return c.client.GetSlot(ctx, rpc.CommitmentProcessed) // get latest height
}

func (c *chainReader) GetLatestBlock(ctx context.Context, commitment rpc.CommitmentType) (*rpc.GetBlockResult, error) {
// get slot based on confirmation
slot, err := c.client.GetSlot(ctx, commitment)
if err != nil {
return nil, err
}

// get block based on slot
version := uint64(0) // pull all tx types (legacy + v0)
return c.client.GetBlockWithOpts(ctx, slot, &rpc.GetBlockOpts{
Commitment: commitment,
MaxSupportedTransactionVersion: &version,
})
}
104 changes: 104 additions & 0 deletions pkg/monitoring/exporter/networkfees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package exporter

import (
"context"
"errors"
"slices"

commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring"
"github.com/smartcontractkit/chainlink-common/pkg/utils/mathutil"
"golang.org/x/exp/constraints"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/fees"
)

func NewNetworkFeesFactory(
lgr commonMonitoring.Logger,
metrics metrics.NetworkFees,
) commonMonitoring.ExporterFactory {
return &networkFeesFactory{
metrics,
lgr,
}
}

type networkFeesFactory struct {
metrics metrics.NetworkFees
lgr commonMonitoring.Logger
}

func (p *networkFeesFactory) NewExporter(
params commonMonitoring.ExporterParams,
) (commonMonitoring.Exporter, error) {
return &networkFees{
params.ChainConfig.GetNetworkName(),
p.metrics,
p.lgr,
}, nil
}

type networkFees struct {
chain string
metrics metrics.NetworkFees
lgr commonMonitoring.Logger
}

func (p *networkFees) Export(ctx context.Context, data interface{}) {
blockData, ok := data.(fees.BlockData)
if !ok {
return // skip if input could not be parsed
}

input := metrics.NetworkFeesInput{}
if err := aggregateFees(input, "computeUnitPrice", blockData.Prices); err != nil {
p.lgr.Errorw("failed to calculate computeUnitPrice", "error", err)
return
}
if err := aggregateFees(input, "totalFee", blockData.Fees); err != nil {
p.lgr.Errorw("failed to calculate totalFee", "error", err)
return
}

p.metrics.Set(input, p.chain)
}

func (p *networkFees) Cleanup(_ context.Context) {
p.metrics.Cleanup()
}

func aggregateFees[V constraints.Integer](input metrics.NetworkFeesInput, name string, data []V) error {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice use of the generic

// skip if empty list
if len(data) == 0 {
return nil
}

slices.Sort(data) // ensure sorted

// calculate median / avg
medianPrice, medianPriceErr := mathutil.Median(data...)
input.Set(name, "median", uint64(medianPrice))
avgPrice, avgPriceErr := mathutil.Avg(data...)
input.Set(name, "avg", uint64(avgPrice))

// calculate lower / upper quartile
var lowerData, upperData []V
l := len(data)
if l%2 == 0 {
lowerData = data[:l/2]
upperData = data[l/2:]
} else {
lowerData = data[:l/2]
upperData = data[l/2+1:]
}
lowerQuartilePrice, lowerQuartilePriceErr := mathutil.Median(lowerData...)
input.Set(name, "lowerQuartile", uint64(lowerQuartilePrice))
upperQuartilePrice, upperQuartilePriceErr := mathutil.Median(upperData...)
input.Set(name, "upperQuartile", uint64(upperQuartilePrice))

// calculate min/max
input.Set(name, "max", uint64(slices.Max(data)))
input.Set(name, "min", uint64(slices.Min(data)))

return errors.Join(medianPriceErr, avgPriceErr, lowerQuartilePriceErr, upperQuartilePriceErr)
}
62 changes: 62 additions & 0 deletions pkg/monitoring/exporter/networkfees_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package exporter

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics"
"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics/mocks"
"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/fees"
)

func TestNetworkFees(t *testing.T) {
ctx := tests.Context(t)
m := mocks.NewNetworkFees(t)
m.On("Set", mock.Anything, mock.Anything).Once()
m.On("Cleanup").Once()

factory := NewNetworkFeesFactory(logger.Test(t), m)

chainConfig := testutils.GenerateChainConfig()
exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig})
require.NoError(t, err)

// happy path
exporter.Export(ctx, fees.BlockData{})
exporter.Cleanup(ctx)

// test passing uint64 instead of NetworkFees - should not call mock
// NetworkFees alias of uint64
exporter.Export(ctx, uint64(10))
}

func TestAggregateFees(t *testing.T) {
input := metrics.NetworkFeesInput{}
v0 := []int{10, 12, 3, 4, 1, 2}
v1 := []int{5, 1, 10, 2, 3, 12, 4}

require.NoError(t, aggregateFees(input, "0", v0))
require.NoError(t, aggregateFees(input, "1", v1))

assert.Equal(t, uint64(3), input["0"]["median"])
assert.Equal(t, uint64(5), input["0"]["avg"])
assert.Equal(t, uint64(1), input["0"]["min"])
assert.Equal(t, uint64(12), input["0"]["max"])
assert.Equal(t, uint64(2), input["0"]["lowerQuartile"])
assert.Equal(t, uint64(10), input["0"]["upperQuartile"])

assert.Equal(t, uint64(4), input["1"]["median"])
assert.Equal(t, uint64(5), input["1"]["avg"])
assert.Equal(t, uint64(1), input["1"]["min"])
assert.Equal(t, uint64(12), input["1"]["max"])
assert.Equal(t, uint64(2), input["1"]["lowerQuartile"])
assert.Equal(t, uint64(10), input["1"]["upperQuartile"])
}
12 changes: 12 additions & 0 deletions pkg/monitoring/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ func init() {
},
[]string{"chain", "url"},
)

// init gauge for network fees
gauges[types.NetworkFeesMetric] = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: types.NetworkFeesMetric,
},
[]string{
"type", // compute budget price, total fee
"operation", // avg, median, upper/lower quartile, min, max
"chain",
},
)
}

type FeedInput struct {
Expand Down
38 changes: 38 additions & 0 deletions pkg/monitoring/metrics/mocks/NetworkFees.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions pkg/monitoring/metrics/networkfees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package metrics

import (
"github.com/prometheus/client_golang/prometheus"
commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types"
)

//go:generate mockery --name NetworkFees --output ./mocks/

type NetworkFees interface {
Set(slot NetworkFeesInput, chain string)
Cleanup()
}

var _ NetworkFees = (*networkFees)(nil)

type networkFees struct {
simpleGauge
labels []prometheus.Labels
}

func NewNetworkFees(log commonMonitoring.Logger) *networkFees {
return &networkFees{
simpleGauge: newSimpleGauge(log, types.NetworkFeesMetric),
}
}

func (sh *networkFees) Set(input NetworkFeesInput, chain string) {
for feeType, opMap := range input {
for operation, value := range opMap {
label := prometheus.Labels{
"type": feeType,
"operation": operation,
"chain": chain,
}
sh.set(float64(value), label)
}
}
sh.labels = input.Labels(chain)
}

func (sh *networkFees) Cleanup() {
for _, l := range sh.labels {
sh.delete(l)
}
}

type NetworkFeesInput map[string]map[string]uint64

func (i NetworkFeesInput) Set(feeType, operation string, value uint64) {
if _, exists := i[feeType]; !exists {
i[feeType] = map[string]uint64{}
}
i[feeType][operation] = value
}

func (i NetworkFeesInput) Labels(chain string) (l []prometheus.Labels) {
for feeType, opMap := range i {
for operation := range opMap {
l = append(l, prometheus.Labels{
"type": feeType,
"operation": operation,
"chain": chain,
})
}
}
return
}
Loading
Loading