From 5e3906fe0e3e8d3d75aae57e0b71aa33c136a047 Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:20:59 -0600 Subject: [PATCH 1/5] wip: slot height tracking --- pkg/monitoring/chain_reader.go | 5 ++++ pkg/monitoring/mocks/ChainReader.go | 24 ++++++++++++++++ pkg/monitoring/source_slotheight.go | 43 +++++++++++++++++++++++++++++ pkg/monitoring/types/types.go | 5 ++++ 4 files changed, 77 insertions(+) create mode 100644 pkg/monitoring/source_slotheight.go create mode 100644 pkg/monitoring/types/types.go diff --git a/pkg/monitoring/chain_reader.go b/pkg/monitoring/chain_reader.go index 760f4f88c..11f19e384 100644 --- a/pkg/monitoring/chain_reader.go +++ b/pkg/monitoring/chain_reader.go @@ -17,6 +17,7 @@ type ChainReader interface { GetBalance(ctx context.Context, account solana.PublicKey, commitment rpc.CommitmentType) (out *rpc.GetBalanceResult, err error) 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) } func NewChainReader(client *rpc.Client) ChainReader { @@ -50,3 +51,7 @@ func (c *chainReader) GetSignaturesForAddressWithOpts(ctx context.Context, accou func (c *chainReader) GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (out *rpc.GetTransactionResult, err error) { return c.client.GetTransaction(ctx, txSig, opts) } + +func (c *chainReader) GetSlot(ctx context.Context) (uint64, error) { + return c.client.GetSlot(ctx, rpc.CommitmentProcessed) // get latest height +} diff --git a/pkg/monitoring/mocks/ChainReader.go b/pkg/monitoring/mocks/ChainReader.go index cec4d9d5f..609625c10 100644 --- a/pkg/monitoring/mocks/ChainReader.go +++ b/pkg/monitoring/mocks/ChainReader.go @@ -102,6 +102,30 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco return r0, r1 } +// GetSlot provides a mock function with given fields: ctx +func (_m *ChainReader) GetSlot(ctx context.Context) (uint64, error) { + ret := _m.Called(ctx) + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetState provides a mock function with given fields: ctx, account, commitment func (_m *ChainReader) GetState(ctx context.Context, account solana.PublicKey, commitment rpc.CommitmentType) (pkgsolana.State, uint64, error) { ret := _m.Called(ctx, account, commitment) diff --git a/pkg/monitoring/source_slotheight.go b/pkg/monitoring/source_slotheight.go new file mode 100644 index 000000000..154110116 --- /dev/null +++ b/pkg/monitoring/source_slotheight.go @@ -0,0 +1,43 @@ +package monitoring + +import ( + "context" + + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +func NewSlotHeightSourceFactory( + client ChainReader, + log commonMonitoring.Logger, +) commonMonitoring.NetworkSourceFactory { + return &slotHeightSourceFactory{ + client, + log, + } +} + +type slotHeightSourceFactory struct { + client ChainReader + log commonMonitoring.Logger +} + +func (s *slotHeightSourceFactory) NewSource( + _ commonMonitoring.ChainConfig, + _ []commonMonitoring.NodeConfig, +) (commonMonitoring.Source, error) { + return &slotHeightSource{s.client}, nil +} + +func (s *slotHeightSourceFactory) GetType() string { + return types.SlotHeightType +} + +type slotHeightSource struct { + client ChainReader +} + +func (t *slotHeightSource) Fetch(ctx context.Context) (interface{}, error) { + return t.client.GetSlot(ctx) // TODO: wrap the type to make it clear which type it is? +} diff --git a/pkg/monitoring/types/types.go b/pkg/monitoring/types/types.go new file mode 100644 index 000000000..b0a358a16 --- /dev/null +++ b/pkg/monitoring/types/types.go @@ -0,0 +1,5 @@ +package types + +const ( + SlotHeightType = "slot_height" +) From 99ea507359e9c96425796e0212a5cd5df9edd77b Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:08:24 -0600 Subject: [PATCH 2/5] add slot height metric + SlotHeight type --- pkg/monitoring/metrics/metrics.go | 8 +++++ pkg/monitoring/metrics/mocks/SlotHeight.go | 38 ++++++++++++++++++++++ pkg/monitoring/metrics/slotheight.go | 37 +++++++++++++++++++++ pkg/monitoring/metrics/slotheight_test.go | 38 ++++++++++++++++++++++ pkg/monitoring/source_slotheight.go | 3 +- pkg/monitoring/types/types.go | 8 ++++- 6 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 pkg/monitoring/metrics/mocks/SlotHeight.go create mode 100644 pkg/monitoring/metrics/slotheight.go create mode 100644 pkg/monitoring/metrics/slotheight_test.go diff --git a/pkg/monitoring/metrics/metrics.go b/pkg/monitoring/metrics/metrics.go index 2768de97a..32d2d44f8 100644 --- a/pkg/monitoring/metrics/metrics.go +++ b/pkg/monitoring/metrics/metrics.go @@ -64,6 +64,14 @@ func init() { }, feedLabels, ) + + // init gauge for slot height + gauges[types.SlotHeightMetric] = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: types.SlotHeightMetric, + }, + []string{"chain", "url"}, + ) } type FeedInput struct { diff --git a/pkg/monitoring/metrics/mocks/SlotHeight.go b/pkg/monitoring/metrics/mocks/SlotHeight.go new file mode 100644 index 000000000..97f008ed1 --- /dev/null +++ b/pkg/monitoring/metrics/mocks/SlotHeight.go @@ -0,0 +1,38 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" + mock "github.com/stretchr/testify/mock" +) + +// SlotHeight is an autogenerated mock type for the SlotHeight type +type SlotHeight struct { + mock.Mock +} + +// Cleanup provides a mock function with given fields: +func (_m *SlotHeight) Cleanup() { + _m.Called() +} + +// Set provides a mock function with given fields: slot, chain, url +func (_m *SlotHeight) Set(slot types.SlotHeight, chain string, url string) { + _m.Called(slot, chain, url) +} + +type mockConstructorTestingTNewSlotHeight interface { + mock.TestingT + Cleanup(func()) +} + +// NewSlotHeight creates a new instance of SlotHeight. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewSlotHeight(t mockConstructorTestingTNewSlotHeight) *SlotHeight { + mock := &SlotHeight{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/monitoring/metrics/slotheight.go b/pkg/monitoring/metrics/slotheight.go new file mode 100644 index 000000000..2c4c5caf5 --- /dev/null +++ b/pkg/monitoring/metrics/slotheight.go @@ -0,0 +1,37 @@ +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 SlotHeight --output ./mocks/ + +type SlotHeight interface { + Set(slot types.SlotHeight, chain, url string) + Cleanup() +} + +var _ SlotHeight = (*slotHeight)(nil) + +type slotHeight struct { + simpleGauge + labels prometheus.Labels +} + +func NewSlotHeight(log commonMonitoring.Logger) *slotHeight { + return &slotHeight{ + simpleGauge: newSimpleGauge(log, types.SlotHeightMetric), + } +} + +func (sh *slotHeight) Set(slot types.SlotHeight, chain, url string) { + sh.labels = prometheus.Labels{"chain": chain, "url": url} + sh.set(float64(slot), sh.labels) +} + +func (sh *slotHeight) Cleanup() { + sh.delete(sh.labels) +} diff --git a/pkg/monitoring/metrics/slotheight_test.go b/pkg/monitoring/metrics/slotheight_test.go new file mode 100644 index 000000000..d795b219b --- /dev/null +++ b/pkg/monitoring/metrics/slotheight_test.go @@ -0,0 +1,38 @@ +package metrics + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +func TestSlotHeight(t *testing.T) { + lgr := logger.Test(t) + m := NewSlotHeight(lgr) + + // fetching gauges + g, ok := gauges[types.SlotHeightMetric] + require.True(t, ok) + + v := 100 + + // set gauge + assert.NotPanics(t, func() { m.Set(types.SlotHeight(v), t.Name(), t.Name()+"_url") }) + promBal := testutil.ToFloat64(g.With(prometheus.Labels{ + "chain": t.Name(), + "url": t.Name() + "_url", + })) + assert.Equal(t, float64(v), promBal) + + // cleanup gauges + assert.Equal(t, 1, testutil.CollectAndCount(g)) + assert.NotPanics(t, func() { m.Cleanup() }) + assert.Equal(t, 0, testutil.CollectAndCount(g)) +} diff --git a/pkg/monitoring/source_slotheight.go b/pkg/monitoring/source_slotheight.go index 154110116..9f0ae94b3 100644 --- a/pkg/monitoring/source_slotheight.go +++ b/pkg/monitoring/source_slotheight.go @@ -39,5 +39,6 @@ type slotHeightSource struct { } func (t *slotHeightSource) Fetch(ctx context.Context) (interface{}, error) { - return t.client.GetSlot(ctx) // TODO: wrap the type to make it clear which type it is? + slot, err := t.client.GetSlot(ctx) + return types.SlotHeight(slot), err } diff --git a/pkg/monitoring/types/types.go b/pkg/monitoring/types/types.go index b0a358a16..665a11b0f 100644 --- a/pkg/monitoring/types/types.go +++ b/pkg/monitoring/types/types.go @@ -1,5 +1,11 @@ package types +// types.go contains simple types, more complex types should have a separate file const ( - SlotHeightType = "slot_height" + SlotHeightType = "slot_height" + SlotHeightMetric = "sol_" + SlotHeightType ) + +// SlotHeight type wraps the uint64 type returned by the RPC call +// this helps to delineate types when sending to the exporter +type SlotHeight uint64 From fada7b51b1edb5f3de567295543d7592ec7a897a Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:24:26 -0600 Subject: [PATCH 3/5] implement exporter --- cmd/monitoring/main.go | 10 +++ pkg/monitoring/exporter/reportobservations.go | 70 +++++----------- .../exporter/reportobservations_test.go | 34 +++----- pkg/monitoring/exporter/slotheight.go | 82 +++++++++++++++++++ pkg/monitoring/exporter/slotheight_test.go | 50 +++++++++++ 5 files changed, 171 insertions(+), 75 deletions(-) create mode 100644 pkg/monitoring/exporter/slotheight.go create mode 100644 pkg/monitoring/exporter/slotheight_test.go diff --git a/cmd/monitoring/main.go b/cmd/monitoring/main.go index d713de5ad..64f1cefb2 100644 --- a/cmd/monitoring/main.go +++ b/cmd/monitoring/main.go @@ -76,8 +76,13 @@ func main() { chainReader, logger.With(log, "component", "source-node-balances"), ) + slotHeightSourceFactory := monitoring.NewSlotHeightSourceFactory( + chainReader, + logger.With(log, "component", "souce-slot-height"), + ) monitor.NetworkSourceFactories = append(monitor.NetworkSourceFactories, nodeBalancesSourceFactory, + slotHeightSourceFactory, ) // per-feed exporters @@ -99,8 +104,13 @@ func main() { logger.With(log, "component", "solana-prom-exporter"), metrics.NewNodeBalances, ) + slotHeightExporterFactory := exporter.NewSlotHeightFactory( + logger.With(log, "component", "solana-prom-exporter"), + metrics.NewSlotHeight(logger.With(log, "component", "solana-metrics")), + ) monitor.NetworkExporterFactories = append(monitor.NetworkExporterFactories, nodeBalancesExporterFactory, + slotHeightExporterFactory, ) monitor.Run() diff --git a/pkg/monitoring/exporter/reportobservations.go b/pkg/monitoring/exporter/reportobservations.go index 80e980c81..6982e3c18 100644 --- a/pkg/monitoring/exporter/reportobservations.go +++ b/pkg/monitoring/exporter/reportobservations.go @@ -8,75 +8,43 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" ) -func NewReportObservationsFactory( - log commonMonitoring.Logger, - metrics metrics.ReportObservations, +func NewSlotHeightFactory( + _ commonMonitoring.Logger, + metrics metrics.SlotHeight, ) commonMonitoring.ExporterFactory { - return &reportObservationsFactory{ - log, + return &slotHeightFactory{ metrics, } } -type reportObservationsFactory struct { - log commonMonitoring.Logger - metrics metrics.ReportObservations +type slotHeightFactory struct { + metrics metrics.SlotHeight } -func (p *reportObservationsFactory) NewExporter( +func (p *slotHeightFactory) NewExporter( params commonMonitoring.ExporterParams, ) (commonMonitoring.Exporter, error) { - return &reportObservations{ - metrics.FeedInput{ - AccountAddress: params.FeedConfig.GetContractAddress(), - FeedID: params.FeedConfig.GetContractAddress(), - ChainID: params.ChainConfig.GetChainID(), - ContractStatus: params.FeedConfig.GetContractStatus(), - ContractType: params.FeedConfig.GetContractType(), - FeedName: params.FeedConfig.GetName(), - FeedPath: params.FeedConfig.GetPath(), - NetworkID: params.ChainConfig.GetNetworkID(), - NetworkName: params.ChainConfig.GetNetworkName(), - }, - p.log, + return &slotHeight{ + params.ChainConfig.GetNetworkName(), + params.ChainConfig.GetRPCEndpoint(), p.metrics, }, nil } -type reportObservations struct { - label metrics.FeedInput // static for each feed - log commonMonitoring.Logger - metrics metrics.ReportObservations +type slotHeight struct { + chain, url string + metrics metrics.SlotHeight } -func (p *reportObservations) Export(ctx context.Context, data interface{}) { - details, err := types.MakeTxDetails(data) - if err != nil { +func (p *slotHeight) Export(ctx context.Context, data interface{}) { + slot, ok := data.(types.SlotHeight) + if !ok { return // skip if input could not be parsed } - // skip on no updates - if len(details) == 0 { - return - } - - // sanity check: find non-empty detail - // assumption: details ordered from latest -> earliest - var latest types.TxDetails - for _, d := range details { - if !d.Empty() { - latest = d - break - } - } - if latest.Empty() { - p.log.Errorw("exporter could not find non-empty TxDetails", "feed", p.label.ToPromLabels()) - return - } - - p.metrics.SetCount(latest.ObservationCount, p.label) + p.metrics.Set(slot, p.chain, p.url) } -func (p *reportObservations) Cleanup(_ context.Context) { - p.metrics.Cleanup(p.label) +func (p *slotHeight) Cleanup(_ context.Context) { + p.metrics.Cleanup() } diff --git a/pkg/monitoring/exporter/reportobservations_test.go b/pkg/monitoring/exporter/reportobservations_test.go index 70726720e..072e27b49 100644 --- a/pkg/monitoring/exporter/reportobservations_test.go +++ b/pkg/monitoring/exporter/reportobservations_test.go @@ -3,10 +3,8 @@ package exporter import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink-common/pkg/logger" commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" @@ -16,35 +14,23 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" ) -func TestReportObservations(t *testing.T) { +func TestSlotHeight(t *testing.T) { ctx := utils.Context(t) - lgr, logs := logger.TestObserved(t, zapcore.ErrorLevel) - m := mocks.NewReportObservations(t) - m.On("SetCount", mock.Anything, mock.Anything).Once() - m.On("Cleanup", mock.Anything).Once() + m := mocks.NewSlotHeight(t) + m.On("Set", mock.Anything, mock.Anything, mock.Anything).Once() + m.On("Cleanup").Once() - factory := NewReportObservationsFactory(lgr, m) + factory := NewSlotHeightFactory(logger.Test(t), m) chainConfig := testutils.GenerateChainConfig() - feedConfig := testutils.GenerateFeedConfig() - exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig, FeedConfig: feedConfig, Nodes: []commonMonitoring.NodeConfig{}}) + exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig}) require.NoError(t, err) // happy path - exporter.Export(ctx, []types.TxDetails{{ObservationCount: 10}}) + exporter.Export(ctx, types.SlotHeight(10)) exporter.Cleanup(ctx) - // not txdetails type - no calls to mock - assert.NotPanics(t, func() { exporter.Export(ctx, 1) }) - - // zero txdetails - no calls to mock - exporter.Export(ctx, []types.TxDetails{}) - - // empty txdetails - exporter.Export(ctx, []types.TxDetails{{}}) - assert.Equal(t, 1, logs.FilterMessage("exporter could not find non-empty TxDetails").Len()) - - // multiple TxDetails should only call for the first non-empty one - m.On("SetCount", uint8(1), mock.Anything).Once() - exporter.Export(ctx, []types.TxDetails{{}, {ObservationCount: 1}, {ObservationCount: 10}}) + // test passing uint64 instead of SlotHeight - should not call mock + // SlotHeight alias of uint64 + exporter.Export(ctx, uint64(10)) } diff --git a/pkg/monitoring/exporter/slotheight.go b/pkg/monitoring/exporter/slotheight.go new file mode 100644 index 000000000..80e980c81 --- /dev/null +++ b/pkg/monitoring/exporter/slotheight.go @@ -0,0 +1,82 @@ +package exporter + +import ( + "context" + + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +func NewReportObservationsFactory( + log commonMonitoring.Logger, + metrics metrics.ReportObservations, +) commonMonitoring.ExporterFactory { + return &reportObservationsFactory{ + log, + metrics, + } +} + +type reportObservationsFactory struct { + log commonMonitoring.Logger + metrics metrics.ReportObservations +} + +func (p *reportObservationsFactory) NewExporter( + params commonMonitoring.ExporterParams, +) (commonMonitoring.Exporter, error) { + return &reportObservations{ + metrics.FeedInput{ + AccountAddress: params.FeedConfig.GetContractAddress(), + FeedID: params.FeedConfig.GetContractAddress(), + ChainID: params.ChainConfig.GetChainID(), + ContractStatus: params.FeedConfig.GetContractStatus(), + ContractType: params.FeedConfig.GetContractType(), + FeedName: params.FeedConfig.GetName(), + FeedPath: params.FeedConfig.GetPath(), + NetworkID: params.ChainConfig.GetNetworkID(), + NetworkName: params.ChainConfig.GetNetworkName(), + }, + p.log, + p.metrics, + }, nil +} + +type reportObservations struct { + label metrics.FeedInput // static for each feed + log commonMonitoring.Logger + metrics metrics.ReportObservations +} + +func (p *reportObservations) Export(ctx context.Context, data interface{}) { + details, err := types.MakeTxDetails(data) + if err != nil { + return // skip if input could not be parsed + } + + // skip on no updates + if len(details) == 0 { + return + } + + // sanity check: find non-empty detail + // assumption: details ordered from latest -> earliest + var latest types.TxDetails + for _, d := range details { + if !d.Empty() { + latest = d + break + } + } + if latest.Empty() { + p.log.Errorw("exporter could not find non-empty TxDetails", "feed", p.label.ToPromLabels()) + return + } + + p.metrics.SetCount(latest.ObservationCount, p.label) +} + +func (p *reportObservations) Cleanup(_ context.Context) { + p.metrics.Cleanup(p.label) +} diff --git a/pkg/monitoring/exporter/slotheight_test.go b/pkg/monitoring/exporter/slotheight_test.go new file mode 100644 index 000000000..70726720e --- /dev/null +++ b/pkg/monitoring/exporter/slotheight_test.go @@ -0,0 +1,50 @@ +package exporter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +func TestReportObservations(t *testing.T) { + ctx := utils.Context(t) + lgr, logs := logger.TestObserved(t, zapcore.ErrorLevel) + m := mocks.NewReportObservations(t) + m.On("SetCount", mock.Anything, mock.Anything).Once() + m.On("Cleanup", mock.Anything).Once() + + factory := NewReportObservationsFactory(lgr, m) + + chainConfig := testutils.GenerateChainConfig() + feedConfig := testutils.GenerateFeedConfig() + exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig, FeedConfig: feedConfig, Nodes: []commonMonitoring.NodeConfig{}}) + require.NoError(t, err) + + // happy path + exporter.Export(ctx, []types.TxDetails{{ObservationCount: 10}}) + exporter.Cleanup(ctx) + + // not txdetails type - no calls to mock + assert.NotPanics(t, func() { exporter.Export(ctx, 1) }) + + // zero txdetails - no calls to mock + exporter.Export(ctx, []types.TxDetails{}) + + // empty txdetails + exporter.Export(ctx, []types.TxDetails{{}}) + assert.Equal(t, 1, logs.FilterMessage("exporter could not find non-empty TxDetails").Len()) + + // multiple TxDetails should only call for the first non-empty one + m.On("SetCount", uint8(1), mock.Anything).Once() + exporter.Export(ctx, []types.TxDetails{{}, {ObservationCount: 1}, {ObservationCount: 10}}) +} From 3dffb07b8d26ae5f134df9c4d0ae1be663af6dda Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:57:12 -0600 Subject: [PATCH 4/5] fix: file naming --- pkg/monitoring/exporter/reportobservations.go | 70 ++++++++++++++----- .../exporter/reportobservations_test.go | 34 ++++++--- pkg/monitoring/exporter/slotheight.go | 70 +++++-------------- pkg/monitoring/exporter/slotheight_test.go | 34 +++------ 4 files changed, 104 insertions(+), 104 deletions(-) diff --git a/pkg/monitoring/exporter/reportobservations.go b/pkg/monitoring/exporter/reportobservations.go index 6982e3c18..80e980c81 100644 --- a/pkg/monitoring/exporter/reportobservations.go +++ b/pkg/monitoring/exporter/reportobservations.go @@ -8,43 +8,75 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" ) -func NewSlotHeightFactory( - _ commonMonitoring.Logger, - metrics metrics.SlotHeight, +func NewReportObservationsFactory( + log commonMonitoring.Logger, + metrics metrics.ReportObservations, ) commonMonitoring.ExporterFactory { - return &slotHeightFactory{ + return &reportObservationsFactory{ + log, metrics, } } -type slotHeightFactory struct { - metrics metrics.SlotHeight +type reportObservationsFactory struct { + log commonMonitoring.Logger + metrics metrics.ReportObservations } -func (p *slotHeightFactory) NewExporter( +func (p *reportObservationsFactory) NewExporter( params commonMonitoring.ExporterParams, ) (commonMonitoring.Exporter, error) { - return &slotHeight{ - params.ChainConfig.GetNetworkName(), - params.ChainConfig.GetRPCEndpoint(), + return &reportObservations{ + metrics.FeedInput{ + AccountAddress: params.FeedConfig.GetContractAddress(), + FeedID: params.FeedConfig.GetContractAddress(), + ChainID: params.ChainConfig.GetChainID(), + ContractStatus: params.FeedConfig.GetContractStatus(), + ContractType: params.FeedConfig.GetContractType(), + FeedName: params.FeedConfig.GetName(), + FeedPath: params.FeedConfig.GetPath(), + NetworkID: params.ChainConfig.GetNetworkID(), + NetworkName: params.ChainConfig.GetNetworkName(), + }, + p.log, p.metrics, }, nil } -type slotHeight struct { - chain, url string - metrics metrics.SlotHeight +type reportObservations struct { + label metrics.FeedInput // static for each feed + log commonMonitoring.Logger + metrics metrics.ReportObservations } -func (p *slotHeight) Export(ctx context.Context, data interface{}) { - slot, ok := data.(types.SlotHeight) - if !ok { +func (p *reportObservations) Export(ctx context.Context, data interface{}) { + details, err := types.MakeTxDetails(data) + if err != nil { return // skip if input could not be parsed } - p.metrics.Set(slot, p.chain, p.url) + // skip on no updates + if len(details) == 0 { + return + } + + // sanity check: find non-empty detail + // assumption: details ordered from latest -> earliest + var latest types.TxDetails + for _, d := range details { + if !d.Empty() { + latest = d + break + } + } + if latest.Empty() { + p.log.Errorw("exporter could not find non-empty TxDetails", "feed", p.label.ToPromLabels()) + return + } + + p.metrics.SetCount(latest.ObservationCount, p.label) } -func (p *slotHeight) Cleanup(_ context.Context) { - p.metrics.Cleanup() +func (p *reportObservations) Cleanup(_ context.Context) { + p.metrics.Cleanup(p.label) } diff --git a/pkg/monitoring/exporter/reportobservations_test.go b/pkg/monitoring/exporter/reportobservations_test.go index 072e27b49..70726720e 100644 --- a/pkg/monitoring/exporter/reportobservations_test.go +++ b/pkg/monitoring/exporter/reportobservations_test.go @@ -3,8 +3,10 @@ package exporter import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink-common/pkg/logger" commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" @@ -14,23 +16,35 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" ) -func TestSlotHeight(t *testing.T) { +func TestReportObservations(t *testing.T) { ctx := utils.Context(t) - m := mocks.NewSlotHeight(t) - m.On("Set", mock.Anything, mock.Anything, mock.Anything).Once() - m.On("Cleanup").Once() + lgr, logs := logger.TestObserved(t, zapcore.ErrorLevel) + m := mocks.NewReportObservations(t) + m.On("SetCount", mock.Anything, mock.Anything).Once() + m.On("Cleanup", mock.Anything).Once() - factory := NewSlotHeightFactory(logger.Test(t), m) + factory := NewReportObservationsFactory(lgr, m) chainConfig := testutils.GenerateChainConfig() - exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig}) + feedConfig := testutils.GenerateFeedConfig() + exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig, FeedConfig: feedConfig, Nodes: []commonMonitoring.NodeConfig{}}) require.NoError(t, err) // happy path - exporter.Export(ctx, types.SlotHeight(10)) + exporter.Export(ctx, []types.TxDetails{{ObservationCount: 10}}) exporter.Cleanup(ctx) - // test passing uint64 instead of SlotHeight - should not call mock - // SlotHeight alias of uint64 - exporter.Export(ctx, uint64(10)) + // not txdetails type - no calls to mock + assert.NotPanics(t, func() { exporter.Export(ctx, 1) }) + + // zero txdetails - no calls to mock + exporter.Export(ctx, []types.TxDetails{}) + + // empty txdetails + exporter.Export(ctx, []types.TxDetails{{}}) + assert.Equal(t, 1, logs.FilterMessage("exporter could not find non-empty TxDetails").Len()) + + // multiple TxDetails should only call for the first non-empty one + m.On("SetCount", uint8(1), mock.Anything).Once() + exporter.Export(ctx, []types.TxDetails{{}, {ObservationCount: 1}, {ObservationCount: 10}}) } diff --git a/pkg/monitoring/exporter/slotheight.go b/pkg/monitoring/exporter/slotheight.go index 80e980c81..6982e3c18 100644 --- a/pkg/monitoring/exporter/slotheight.go +++ b/pkg/monitoring/exporter/slotheight.go @@ -8,75 +8,43 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" ) -func NewReportObservationsFactory( - log commonMonitoring.Logger, - metrics metrics.ReportObservations, +func NewSlotHeightFactory( + _ commonMonitoring.Logger, + metrics metrics.SlotHeight, ) commonMonitoring.ExporterFactory { - return &reportObservationsFactory{ - log, + return &slotHeightFactory{ metrics, } } -type reportObservationsFactory struct { - log commonMonitoring.Logger - metrics metrics.ReportObservations +type slotHeightFactory struct { + metrics metrics.SlotHeight } -func (p *reportObservationsFactory) NewExporter( +func (p *slotHeightFactory) NewExporter( params commonMonitoring.ExporterParams, ) (commonMonitoring.Exporter, error) { - return &reportObservations{ - metrics.FeedInput{ - AccountAddress: params.FeedConfig.GetContractAddress(), - FeedID: params.FeedConfig.GetContractAddress(), - ChainID: params.ChainConfig.GetChainID(), - ContractStatus: params.FeedConfig.GetContractStatus(), - ContractType: params.FeedConfig.GetContractType(), - FeedName: params.FeedConfig.GetName(), - FeedPath: params.FeedConfig.GetPath(), - NetworkID: params.ChainConfig.GetNetworkID(), - NetworkName: params.ChainConfig.GetNetworkName(), - }, - p.log, + return &slotHeight{ + params.ChainConfig.GetNetworkName(), + params.ChainConfig.GetRPCEndpoint(), p.metrics, }, nil } -type reportObservations struct { - label metrics.FeedInput // static for each feed - log commonMonitoring.Logger - metrics metrics.ReportObservations +type slotHeight struct { + chain, url string + metrics metrics.SlotHeight } -func (p *reportObservations) Export(ctx context.Context, data interface{}) { - details, err := types.MakeTxDetails(data) - if err != nil { +func (p *slotHeight) Export(ctx context.Context, data interface{}) { + slot, ok := data.(types.SlotHeight) + if !ok { return // skip if input could not be parsed } - // skip on no updates - if len(details) == 0 { - return - } - - // sanity check: find non-empty detail - // assumption: details ordered from latest -> earliest - var latest types.TxDetails - for _, d := range details { - if !d.Empty() { - latest = d - break - } - } - if latest.Empty() { - p.log.Errorw("exporter could not find non-empty TxDetails", "feed", p.label.ToPromLabels()) - return - } - - p.metrics.SetCount(latest.ObservationCount, p.label) + p.metrics.Set(slot, p.chain, p.url) } -func (p *reportObservations) Cleanup(_ context.Context) { - p.metrics.Cleanup(p.label) +func (p *slotHeight) Cleanup(_ context.Context) { + p.metrics.Cleanup() } diff --git a/pkg/monitoring/exporter/slotheight_test.go b/pkg/monitoring/exporter/slotheight_test.go index 70726720e..072e27b49 100644 --- a/pkg/monitoring/exporter/slotheight_test.go +++ b/pkg/monitoring/exporter/slotheight_test.go @@ -3,10 +3,8 @@ package exporter import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink-common/pkg/logger" commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" @@ -16,35 +14,23 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" ) -func TestReportObservations(t *testing.T) { +func TestSlotHeight(t *testing.T) { ctx := utils.Context(t) - lgr, logs := logger.TestObserved(t, zapcore.ErrorLevel) - m := mocks.NewReportObservations(t) - m.On("SetCount", mock.Anything, mock.Anything).Once() - m.On("Cleanup", mock.Anything).Once() + m := mocks.NewSlotHeight(t) + m.On("Set", mock.Anything, mock.Anything, mock.Anything).Once() + m.On("Cleanup").Once() - factory := NewReportObservationsFactory(lgr, m) + factory := NewSlotHeightFactory(logger.Test(t), m) chainConfig := testutils.GenerateChainConfig() - feedConfig := testutils.GenerateFeedConfig() - exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig, FeedConfig: feedConfig, Nodes: []commonMonitoring.NodeConfig{}}) + exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig}) require.NoError(t, err) // happy path - exporter.Export(ctx, []types.TxDetails{{ObservationCount: 10}}) + exporter.Export(ctx, types.SlotHeight(10)) exporter.Cleanup(ctx) - // not txdetails type - no calls to mock - assert.NotPanics(t, func() { exporter.Export(ctx, 1) }) - - // zero txdetails - no calls to mock - exporter.Export(ctx, []types.TxDetails{}) - - // empty txdetails - exporter.Export(ctx, []types.TxDetails{{}}) - assert.Equal(t, 1, logs.FilterMessage("exporter could not find non-empty TxDetails").Len()) - - // multiple TxDetails should only call for the first non-empty one - m.On("SetCount", uint8(1), mock.Anything).Once() - exporter.Export(ctx, []types.TxDetails{{}, {ObservationCount: 1}, {ObservationCount: 10}}) + // test passing uint64 instead of SlotHeight - should not call mock + // SlotHeight alias of uint64 + exporter.Export(ctx, uint64(10)) } From 4c60dc2ed04be44d726d3b6837d464a7d816f655 Mon Sep 17 00:00:00 2001 From: aalu1418 <50029043+aalu1418@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:08:36 -0600 Subject: [PATCH 5/5] share naming + test cov --- cmd/monitoring/main.go | 16 ++++++----- pkg/monitoring/source_slotheight_test.go | 34 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 pkg/monitoring/source_slotheight_test.go diff --git a/cmd/monitoring/main.go b/cmd/monitoring/main.go index 64f1cefb2..cd87ae5f7 100644 --- a/cmd/monitoring/main.go +++ b/cmd/monitoring/main.go @@ -85,14 +85,18 @@ func main() { slotHeightSourceFactory, ) + // exporter names + promExporter := "solana-prom-exporter" + promMetrics := "solana-metrics" + // per-feed exporters feedBalancesExporterFactory := exporter.NewFeedBalancesFactory( - logger.With(log, "component", "solana-prom-exporter"), - metrics.NewFeedBalances(logger.With(log, "component", "solana-metrics")), + logger.With(log, "component", promExporter), + metrics.NewFeedBalances(logger.With(log, "component", promMetrics)), ) reportObservationsFactory := exporter.NewReportObservationsFactory( logger.With(log, "component", "solana-prome-exporter"), - metrics.NewReportObservations(logger.With(log, "component", "solana-metrics")), + metrics.NewReportObservations(logger.With(log, "component", promMetrics)), ) monitor.ExporterFactories = append(monitor.ExporterFactories, feedBalancesExporterFactory, @@ -101,12 +105,12 @@ func main() { // network exporters nodeBalancesExporterFactory := exporter.NewNodeBalancesFactory( - logger.With(log, "component", "solana-prom-exporter"), + logger.With(log, "component", promExporter), metrics.NewNodeBalances, ) slotHeightExporterFactory := exporter.NewSlotHeightFactory( - logger.With(log, "component", "solana-prom-exporter"), - metrics.NewSlotHeight(logger.With(log, "component", "solana-metrics")), + logger.With(log, "component", promExporter), + metrics.NewSlotHeight(logger.With(log, "component", promMetrics)), ) monitor.NetworkExporterFactories = append(monitor.NetworkExporterFactories, nodeBalancesExporterFactory, diff --git a/pkg/monitoring/source_slotheight_test.go b/pkg/monitoring/source_slotheight_test.go new file mode 100644 index 000000000..45c704748 --- /dev/null +++ b/pkg/monitoring/source_slotheight_test.go @@ -0,0 +1,34 @@ +package monitoring + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +func TestSlotHeightSource(t *testing.T) { + cr := mocks.NewChainReader(t) + lgr := logger.Test(t) + ctx := utils.Context(t) + + factory := NewSlotHeightSourceFactory(cr, lgr) + assert.Equal(t, types.SlotHeightType, factory.GetType()) + + // generate source + source, err := factory.NewSource(nil, nil) + cr.On("GetSlot", mock.Anything, mock.Anything, mock.Anything).Return(uint64(1), nil).Once() + + // happy path + out, err := source.Fetch(ctx) + require.NoError(t, err) + slot, ok := out.(types.SlotHeight) + require.True(t, ok) + assert.Equal(t, types.SlotHeight(1), slot) +}