diff --git a/cmd/monitoring/main.go b/cmd/monitoring/main.go index 955f3d9ea..219df2fca 100644 --- a/cmd/monitoring/main.go +++ b/cmd/monitoring/main.go @@ -94,17 +94,22 @@ func main() { metrics.NewFeedBalances(logger.With(log, "component", promMetrics)), ) reportObservationsFactory := exporter.NewReportObservationsFactory( - logger.With(log, "component", "solana-prome-exporter"), + logger.With(log, "component", promExporter), metrics.NewReportObservations(logger.With(log, "component", promMetrics)), ) feesFactory := exporter.NewFeesFactory( - logger.With(log, "component", "solana-prome-exporter"), + logger.With(log, "component", promExporter), metrics.NewFees(logger.With(log, "component", promMetrics)), ) + nodeSuccessFactory := exporter.NewNodeSuccessFactory( + logger.With(log, "component", promExporter), + metrics.NewNodeSuccess(logger.With(log, "component", promMetrics)), + ) monitor.ExporterFactories = append(monitor.ExporterFactories, feedBalancesExporterFactory, reportObservationsFactory, feesFactory, + nodeSuccessFactory, ) // network exporters diff --git a/integration-tests/common/common.go b/integration-tests/common/common.go index f9528fb34..3ae27f606 100644 --- a/integration-tests/common/common.go +++ b/integration-tests/common/common.go @@ -27,25 +27,18 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/chainlink" mock_adapter "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/mock-adapter" "github.com/smartcontractkit/chainlink-testing-framework/k8s/pkg/helm/sol" - "github.com/smartcontractkit/chainlink-testing-framework/utils/ptr" "github.com/smartcontractkit/chainlink/integration-tests/client" "github.com/smartcontractkit/chainlink/integration-tests/contracts" "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" - "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" - cl "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/store/models" - "github.com/smartcontractkit/chainlink-common/pkg/config" - commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" - chainConfig "github.com/smartcontractkit/chainlink-solana/integration-tests/config" test_env_sol "github.com/smartcontractkit/chainlink-solana/integration-tests/docker/testenv" "github.com/smartcontractkit/chainlink-solana/integration-tests/solclient" tc "github.com/smartcontractkit/chainlink-solana/integration-tests/testconfig" - solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" ) type Common struct { @@ -170,6 +163,9 @@ func New(testConfig *tc.TestConfig) *Common { }, Env: &environment.Environment{}, } + // provide getters for TestConfig (pointers to chain details) + c.TestConfig.GetChainID = func() string { return c.ChainDetails.ChainID } + c.TestConfig.GetURL = func() string { return c.ChainDetails.RPCUrl } return c } @@ -483,32 +479,6 @@ func BuildNodeContractPairID(node *client.ChainlinkClient, ocr2Addr string) (str return strings.ToLower(fmt.Sprintf("node_%s_contract_%s", shortNodeAddr, shortOCRAddr)), nil } -func (c *Common) DefaultNodeConfig() *cl.Config { - solConfig := solcfg.TOMLConfig{ - Enabled: ptr.Ptr(true), - ChainID: ptr.Ptr(c.ChainDetails.ChainID), - Nodes: []*solcfg.Node{ - { - Name: ptr.Ptr("primary"), - URL: config.MustParseURL(c.ChainDetails.RPCUrl), - }, - }, - } - baseConfig := node.NewBaseConfig() - baseConfig.Solana = solcfg.TOMLConfigs{ - &solConfig, - } - baseConfig.OCR2.Enabled = ptr.Ptr(true) - baseConfig.P2P.V2.Enabled = ptr.Ptr(true) - fiveSecondDuration := commonconfig.MustNewDuration(5 * time.Second) - - baseConfig.P2P.V2.DeltaDial = fiveSecondDuration - baseConfig.P2P.V2.DeltaReconcile = fiveSecondDuration - baseConfig.P2P.V2.ListenAddresses = &[]string{"0.0.0.0:6690"} - - return baseConfig -} - func (c *Common) Default(t *testing.T, namespacePrefix string) (*Common, error) { c.TestEnvDetails.K8Config = &environment.Config{ NamespacePrefix: fmt.Sprintf("solana-%s", namespacePrefix), @@ -517,8 +487,7 @@ func (c *Common) Default(t *testing.T, namespacePrefix string) (*Common, error) } if *c.TestConfig.Common.InsideK8s { - toml := c.DefaultNodeConfig() - tomlString, err := toml.TOMLString() + tomlString, err := c.TestConfig.GetNodeConfigTOML() if err != nil { return nil, err } diff --git a/integration-tests/common/test_common.go b/integration-tests/common/test_common.go index 1a5d34944..20565ec4d 100644 --- a/integration-tests/common/test_common.go +++ b/integration-tests/common/test_common.go @@ -142,7 +142,6 @@ func (m *OCRv2TestState) DeployCluster(contractsDir string) { WithTestInstance(m.Config.T). WithTestConfig(m.Config.TestConfig). WithMockAdapter(). - WithCLNodeConfig(m.Common.DefaultNodeConfig()). WithCLNodes(*m.Config.TestConfig.OCR2.NodeCount). WithCLNodeOptions(m.Common.TestEnvDetails.NodeOpts...). WithStandardCleanup(). diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 1f52be8f3..c3d346b66 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -15,10 +15,10 @@ require ( github.com/pelletier/go-toml/v2 v2.1.1 github.com/rs/zerolog v1.30.0 github.com/smartcontractkit/chainlink-common v0.1.7-0.20240524093536-4faf6e011d6d - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240523174813-45db170c1ccc + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524131846-a10ff1f030c9 github.com/smartcontractkit/chainlink-testing-framework v1.28.15 - github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240515225456-aeb9f4d50d65 - github.com/smartcontractkit/chainlink/v2 v2.10.0-beta0.0.20240524101405-efc810b13911 + github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240524135402-6bd866a31530 + github.com/smartcontractkit/chainlink/v2 v2.10.0-beta0.0.20240524135402-6bd866a31530 github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c github.com/smartcontractkit/seth v1.0.9 github.com/stretchr/testify v1.9.0 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 9e85b5a35..c45878478 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1422,10 +1422,10 @@ github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-202403282 github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-20240328204215-ac91f55f1449/go.mod h1:DC8sQMyTlI/44UCTL8QWFwb0bYNoXCfjwCv2hMivYZU= github.com/smartcontractkit/chainlink-vrf v0.0.0-20240222010609-cd67d123c772 h1:LQmRsrzzaYYN3wEU1l5tWiccznhvbyGnu2N+wHSXZAo= github.com/smartcontractkit/chainlink-vrf v0.0.0-20240222010609-cd67d123c772/go.mod h1:Kn1Hape05UzFZ7bOUnm3GVsHzP0TNrVmpfXYNHdqGGs= -github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240515225456-aeb9f4d50d65 h1:8AoBDPHOLgZA1JodqysYK/JxcVbjwNhyGfmwzQuep4s= -github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240515225456-aeb9f4d50d65/go.mod h1:DOeyxJuvSV8No26UHAtmvZTycuGe0S4w/XMMj1EGMV8= -github.com/smartcontractkit/chainlink/v2 v2.10.0-beta0.0.20240524101405-efc810b13911 h1:txjA8yF/ztl/B3BgOIyUfoEnw7k4JJwRQvQ2qZtFqPg= -github.com/smartcontractkit/chainlink/v2 v2.10.0-beta0.0.20240524101405-efc810b13911/go.mod h1:N+101og7dx8dY81uqMJqpCqu56IY2G55DoYbMGwvruM= +github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240524135402-6bd866a31530 h1:QmIxClaisbyLQ06Ztbq+u1pvd9j/7fDC2QKHq3GbFQg= +github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240524135402-6bd866a31530/go.mod h1:+a2IIu/Q1+fB9mPkbjNyYUZcM91YDYfivzByE9kDIco= +github.com/smartcontractkit/chainlink/v2 v2.10.0-beta0.0.20240524135402-6bd866a31530 h1:yEddXtOMcIlx+UbI+DfeFcYdyd3xM8/xlCl1Qa9W7ik= +github.com/smartcontractkit/chainlink/v2 v2.10.0-beta0.0.20240524135402-6bd866a31530/go.mod h1:GoS6m5Bn8aAYLgy2231pxbfr4fSD3rmJBd0Jg2m+gvs= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306 h1:ko88+ZznniNJZbZPWAvHQU8SwKAdHngdDZ+pvVgB5ss= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= diff --git a/integration-tests/testconfig/testconfig.go b/integration-tests/testconfig/testconfig.go index f81de5806..3c7e6c946 100644 --- a/integration-tests/testconfig/testconfig.go +++ b/integration-tests/testconfig/testconfig.go @@ -5,8 +5,10 @@ import ( "encoding/base64" "errors" "fmt" + "log" "os" "strings" + "time" "github.com/barkimedes/go-deepcopy" "github.com/google/uuid" @@ -15,14 +17,18 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" "github.com/smartcontractkit/seth" ctf_config "github.com/smartcontractkit/chainlink-testing-framework/config" k8s_config "github.com/smartcontractkit/chainlink-testing-framework/k8s/config" "github.com/smartcontractkit/chainlink-testing-framework/logging" "github.com/smartcontractkit/chainlink-testing-framework/utils/osutil" + "github.com/smartcontractkit/chainlink-testing-framework/utils/ptr" ocr2_config "github.com/smartcontractkit/chainlink-solana/integration-tests/testconfig/ocr2" + solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" ) type TestConfig struct { @@ -34,6 +40,10 @@ type TestConfig struct { OCR2 *ocr2_config.Config `toml:"OCR2"` SolanaConfig *SolanaConfig `toml:"SolanaConfig"` ConfigurationName string `toml:"-"` + + // getter funcs for passing parameters + GetChainID func() string + GetURL func() string } func (c *TestConfig) GetLoggingConfig() *ctf_config.LoggingConfig { @@ -53,7 +63,49 @@ func (c *TestConfig) GetSethConfig() *seth.Config { } func (c *TestConfig) GetNodeConfig() *ctf_config.NodeConfig { - return nil + cfgTOML, err := c.GetNodeConfigTOML() + if err != nil { + log.Fatalf("failed to parse TOML config: %s", err) + return nil + } + + return &ctf_config.NodeConfig{ + BaseConfigTOML: cfgTOML, + } +} + +func (c *TestConfig) GetNodeConfigTOML() (string, error) { + var chainID, url string + if c.GetChainID != nil { + chainID = c.GetChainID() + } + if c.GetURL != nil { + url = c.GetURL() + } + + solConfig := solcfg.TOMLConfig{ + Enabled: ptr.Ptr(true), + ChainID: ptr.Ptr(chainID), + Nodes: []*solcfg.Node{ + { + Name: ptr.Ptr("primary"), + URL: config.MustParseURL(url), + }, + }, + } + baseConfig := node.NewBaseConfig() + baseConfig.Solana = solcfg.TOMLConfigs{ + &solConfig, + } + baseConfig.OCR2.Enabled = ptr.Ptr(true) + baseConfig.P2P.V2.Enabled = ptr.Ptr(true) + fiveSecondDuration := config.MustNewDuration(5 * time.Second) + + baseConfig.P2P.V2.DeltaDial = fiveSecondDuration + baseConfig.P2P.V2.DeltaReconcile = fiveSecondDuration + baseConfig.P2P.V2.ListenAddresses = &[]string{"0.0.0.0:6690"} + + return baseConfig.TOMLString() } var embeddedConfigs embed.FS diff --git a/pkg/monitoring/exporter/nodesuccess.go b/pkg/monitoring/exporter/nodesuccess.go new file mode 100644 index 000000000..8c3b660b2 --- /dev/null +++ b/pkg/monitoring/exporter/nodesuccess.go @@ -0,0 +1,111 @@ +package exporter + +import ( + "context" + + "github.com/gagliardetto/solana-go" + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/config" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +func NewNodeSuccessFactory( + log commonMonitoring.Logger, + metrics metrics.NodeSuccess, +) commonMonitoring.ExporterFactory { + return &nodeSuccessFactory{ + log, + metrics, + } +} + +type nodeSuccessFactory struct { + log commonMonitoring.Logger + metrics metrics.NodeSuccess +} + +func (p *nodeSuccessFactory) NewExporter( + params commonMonitoring.ExporterParams, +) (commonMonitoring.Exporter, error) { + nodes, err := config.MakeSolanaNodeConfigs(params.Nodes) + if err != nil { + return nil, err + } + + nodesMap := map[solana.PublicKey]string{} + for _, v := range nodes { + pubkey, err := v.PublicKey() + if err != nil { + return nil, err + } + nodesMap[pubkey] = v.GetName() + } + + return &nodeSuccess{ + 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(), + }, + nodesMap, + p.log, + p.metrics, + }, nil +} + +type nodeSuccess struct { + feedLabel metrics.FeedInput // static for each feed + nodes map[solana.PublicKey]string + log commonMonitoring.Logger + metrics metrics.NodeSuccess +} + +func (p *nodeSuccess) 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 + } + + // calculate count + count := map[solana.PublicKey]int{} + for _, d := range details { + count[d.Sender]++ + } + + for k, v := range count { + name, isOperator := p.nodes[k] + if !isOperator { + p.log.Debugw("Sender does not match known operator", "sender", k) + continue // skip if not known operator + } + + p.metrics.Add(v, metrics.NodeFeedInput{ + NodeAddress: k.String(), + NodeOperator: name, + FeedInput: p.feedLabel, + }) + } +} + +func (p *nodeSuccess) Cleanup(_ context.Context) { + for k, v := range p.nodes { + p.metrics.Cleanup(metrics.NodeFeedInput{ + NodeAddress: k.String(), + NodeOperator: v, + FeedInput: p.feedLabel, + }) + } +} diff --git a/pkg/monitoring/exporter/nodesuccess_test.go b/pkg/monitoring/exporter/nodesuccess_test.go new file mode 100644 index 000000000..3f6f8bfa0 --- /dev/null +++ b/pkg/monitoring/exporter/nodesuccess_test.go @@ -0,0 +1,55 @@ +package exporter + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "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/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/config" + "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 TestNodeSuccess(t *testing.T) { + zeroAddress := solana.PublicKey{} + ctx := tests.Context(t) + lgr, logs := logger.TestObserved(t, zapcore.DebugLevel) + m := mocks.NewNodeSuccess(t) + m.On("Add", mock.Anything, mock.Anything).Once() + m.On("Cleanup", mock.Anything).Once() + + factory := NewNodeSuccessFactory(lgr, m) + + chainConfig := testutils.GenerateChainConfig() + feedConfig := testutils.GenerateFeedConfig() + exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig, + FeedConfig: feedConfig, + Nodes: []commonMonitoring.NodeConfig{ + config.SolanaNodeConfig{ + NodeAddress: []string{zeroAddress.String()}}, + }}) + require.NoError(t, err) + + // happy path - only one call (only 1 address is recognized) + exporter.Export(ctx, []types.TxDetails{ + {Sender: zeroAddress}, + {Sender: solana.PublicKey{1}}, + }) + exporter.Cleanup(ctx) + assert.Equal(t, 1, logs.FilterMessageSnippet("Sender does not match known operator").Len()) + + // 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{}) +} diff --git a/pkg/monitoring/metrics/common.go b/pkg/monitoring/metrics/common.go index 0de63e97d..5ddf868a7 100644 --- a/pkg/monitoring/metrics/common.go +++ b/pkg/monitoring/metrics/common.go @@ -20,30 +20,30 @@ func newSimpleGauge(log commonMonitoring.Logger, name string) simpleGauge { return simpleGauge{log, name} } -func (sg simpleGauge) set(value float64, labels prometheus.Labels) { +func (sg simpleGauge) run( + f func(*prometheus.GaugeVec), +) { if gauges == nil { sg.log.Fatalw("gauges is nil") return } gauge, ok := gauges[sg.metricName] - if !ok { + if !ok || gauge == nil { sg.log.Errorw("gauge not found", "name", sg.metricName) return } - gauge.With(labels).Set(value) + f(gauge) +} + +func (sg simpleGauge) set(value float64, labels prometheus.Labels) { + sg.run(func(g *prometheus.GaugeVec) { g.With(labels).Set(value) }) } func (sg simpleGauge) delete(labels prometheus.Labels) { - if gauges == nil { - sg.log.Fatalw("gauges is nil") - return - } + sg.run(func(g *prometheus.GaugeVec) { g.Delete(labels) }) +} - gauge, ok := gauges[sg.metricName] - if !ok { - sg.log.Errorw("gauge not found", "name", sg.metricName) - return - } - gauge.Delete(labels) +func (sg simpleGauge) add(value float64, labels prometheus.Labels) { + sg.run(func(g *prometheus.GaugeVec) { g.With(labels).Add(value) }) } diff --git a/pkg/monitoring/metrics/metrics.go b/pkg/monitoring/metrics/metrics.go index e258854bd..b108736eb 100644 --- a/pkg/monitoring/metrics/metrics.go +++ b/pkg/monitoring/metrics/metrics.go @@ -23,6 +23,11 @@ var ( "network_name", } + nodeFeedLabels = append([]string{ + "node_address", + "node_operator", + }, feedLabels...) + nodeLabels = []string{ "account_address", "node_operator", @@ -67,6 +72,14 @@ func init() { ) } + // init gauge for node success per feed per node + gauges[types.NodeSuccessMetric] = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: types.NodeSuccessMetric, + }, + nodeFeedLabels, + ) + // init gauge for slot height gauges[types.SlotHeightMetric] = promauto.NewGaugeVec( prometheus.GaugeOpts{ @@ -93,3 +106,15 @@ func (i FeedInput) ToPromLabels() prometheus.Labels { "network_name": i.NetworkName, } } + +type NodeFeedInput struct { + NodeAddress, NodeOperator string + FeedInput +} + +func (i NodeFeedInput) ToPromLabels() prometheus.Labels { + l := i.FeedInput.ToPromLabels() + l["node_address"] = i.NodeAddress + l["node_operator"] = i.NodeOperator + return l +} diff --git a/pkg/monitoring/metrics/mocks/NodeSuccess.go b/pkg/monitoring/metrics/mocks/NodeSuccess.go new file mode 100644 index 000000000..10c336db1 --- /dev/null +++ b/pkg/monitoring/metrics/mocks/NodeSuccess.go @@ -0,0 +1,38 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package mocks + +import ( + metrics "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics" + mock "github.com/stretchr/testify/mock" +) + +// NodeSuccess is an autogenerated mock type for the NodeSuccess type +type NodeSuccess struct { + mock.Mock +} + +// Add provides a mock function with given fields: count, i +func (_m *NodeSuccess) Add(count int, i metrics.NodeFeedInput) { + _m.Called(count, i) +} + +// Cleanup provides a mock function with given fields: i +func (_m *NodeSuccess) Cleanup(i metrics.NodeFeedInput) { + _m.Called(i) +} + +type mockConstructorTestingTNewNodeSuccess interface { + mock.TestingT + Cleanup(func()) +} + +// NewNodeSuccess creates a new instance of NodeSuccess. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewNodeSuccess(t mockConstructorTestingTNewNodeSuccess) *NodeSuccess { + mock := &NodeSuccess{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/monitoring/metrics/nodesuccess.go b/pkg/monitoring/metrics/nodesuccess.go new file mode 100644 index 000000000..73cc00d94 --- /dev/null +++ b/pkg/monitoring/metrics/nodesuccess.go @@ -0,0 +1,32 @@ +package metrics + +import ( + commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring" + + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types" +) + +//go:generate mockery --name NodeSuccess --output ./mocks/ + +type NodeSuccess interface { + Add(count int, i NodeFeedInput) + Cleanup(i NodeFeedInput) +} + +var _ NodeSuccess = (*nodeSuccess)(nil) + +type nodeSuccess struct { + simpleGauge +} + +func NewNodeSuccess(log commonMonitoring.Logger) *nodeSuccess { + return &nodeSuccess{newSimpleGauge(log, types.NodeSuccessMetric)} +} + +func (ro *nodeSuccess) Add(count int, i NodeFeedInput) { + ro.add(float64(count), i.ToPromLabels()) +} + +func (ro *nodeSuccess) Cleanup(i NodeFeedInput) { + ro.delete(i.ToPromLabels()) +} diff --git a/pkg/monitoring/metrics/nodesuccess_test.go b/pkg/monitoring/metrics/nodesuccess_test.go new file mode 100644 index 000000000..44f238fdf --- /dev/null +++ b/pkg/monitoring/metrics/nodesuccess_test.go @@ -0,0 +1,36 @@ +package metrics + +import ( + "testing" + + "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 TestNodeSuccess(t *testing.T) { + lgr := logger.Test(t) + m := NewNodeSuccess(lgr) + + // fetching gauges + g, ok := gauges[types.NodeSuccessMetric] + require.True(t, ok) + + v := 100 + inputs := NodeFeedInput{FeedInput: FeedInput{NetworkName: t.Name()}} + + // set gauge + assert.NotPanics(t, func() { m.Add(v, inputs) }) + assert.NotPanics(t, func() { m.Add(v, inputs) }) + promBal := testutil.ToFloat64(g.With(inputs.ToPromLabels())) + assert.Equal(t, float64(2*v), promBal) + + // cleanup gauges + assert.Equal(t, 1, testutil.CollectAndCount(g)) + assert.NotPanics(t, func() { m.Cleanup(inputs) }) + assert.Equal(t, 0, testutil.CollectAndCount(g)) +} diff --git a/pkg/monitoring/types/txdetails.go b/pkg/monitoring/types/txdetails.go index 7788aca28..f5149677a 100644 --- a/pkg/monitoring/types/txdetails.go +++ b/pkg/monitoring/types/txdetails.go @@ -19,7 +19,9 @@ var ( ReportObservationMetric = "report_observations" TxFeeMetric = "tx_fee" ComputeUnitPriceMetric = "tx_compute_unit_price" + NodeSuccessMetric = "node_success" // per node per feed + // these metrics are per feed TxDetailsMetrics = []string{ ReportObservationMetric, TxFeeMetric, diff --git a/pkg/solana/config/toml.go b/pkg/solana/config/toml.go index 96302a45d..04cf0a08f 100644 --- a/pkg/solana/config/toml.go +++ b/pkg/solana/config/toml.go @@ -21,13 +21,11 @@ func (cs TOMLConfigs) ValidateConfig() (err error) { } func (cs TOMLConfigs) validateKeys() (err error) { - errA := []error{} // goal: remove and go back to only errors.Join (https://smartcontract-it.atlassian.net/browse/BCI-3330) - // Unique chain IDs chainIDs := config.UniqueStrings{} for i, c := range cs { if chainIDs.IsDupe(c.ChainID) { - errA = append(errA, config.NewErrDuplicate(fmt.Sprintf("%d.ChainID", i), *c.ChainID)) + err = errors.Join(err, config.NewErrDuplicate(fmt.Sprintf("%d.ChainID", i), *c.ChainID)) } } @@ -36,7 +34,7 @@ func (cs TOMLConfigs) validateKeys() (err error) { for i, c := range cs { for j, n := range c.Nodes { if names.IsDupe(n.Name) { - errA = append(errA, config.NewErrDuplicate(fmt.Sprintf("%d.Nodes.%d.Name", i, j), *n.Name)) + err = errors.Join(err, config.NewErrDuplicate(fmt.Sprintf("%d.Nodes.%d.Name", i, j), *n.Name)) } } } @@ -47,11 +45,11 @@ func (cs TOMLConfigs) validateKeys() (err error) { for j, n := range c.Nodes { u := (*url.URL)(n.URL) if urls.IsDupeFmt(u) { - errA = append(errA, config.NewErrDuplicate(fmt.Sprintf("%d.Nodes.%d.URL", i, j), u.String())) + err = errors.Join(err, config.NewErrDuplicate(fmt.Sprintf("%d.Nodes.%d.URL", i, j), u.String())) } } } - return errors.Join(errA...) + return } func (cs *TOMLConfigs) SetFrom(fs *TOMLConfigs) (err error) { @@ -181,18 +179,16 @@ func setFromChain(c, f *Chain) { } func (c *TOMLConfig) ValidateConfig() (err error) { - errA := []error{} - if c.ChainID == nil { - errA = append(errA, config.ErrMissing{Name: "ChainID", Msg: "required for all chains"}) + err = errors.Join(err, config.ErrMissing{Name: "ChainID", Msg: "required for all chains"}) } else if *c.ChainID == "" { - errA = append(errA, config.ErrEmpty{Name: "ChainID", Msg: "required for all chains"}) + err = errors.Join(err, config.ErrEmpty{Name: "ChainID", Msg: "required for all chains"}) } if len(c.Nodes) == 0 { - errA = append(errA, config.ErrMissing{Name: "Nodes", Msg: "must have at least one node"}) + err = errors.Join(err, config.ErrMissing{Name: "Nodes", Msg: "must have at least one node"}) } - return errors.Join(errA...) + return } func (c *TOMLConfig) TOMLString() (string, error) {