diff --git a/chainio/clients/eth/instrumented_client.go b/chainio/clients/eth/instrumented_client.go index 30c2fcf8..45eeac08 100644 --- a/chainio/clients/eth/instrumented_client.go +++ b/chainio/clients/eth/instrumented_client.go @@ -5,23 +5,23 @@ import ( "math/big" "time" - "github.com/Layr-Labs/eigensdk-go/metrics" + rpccalls "github.com/Layr-Labs/eigensdk-go/metrics/collectors/rpc_calls" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ) -// InstrumentedClient is a wrapper around the geth ethclient that instruments +// InstrumentedClient is a wrapper around the geth ethclient that instruments // all the calls made to it. It counts each eth_ call made to it, and records the duration of each call, // and exposes these as prometheus metrics -// +// // TODO: ideally this should be done at the rpcclient level, not the ethclient level, which would make // our life much easier... but geth implemented the gethclient using an rpcClient struct instead of interface... // see https://github.com/ethereum/go-ethereum/issues/28267 type InstrumentedClient struct { - client *ethclient.Client - metrics metrics.Metrics + client *ethclient.Client + rpcCallsCollector *rpccalls.Collector // we store both client and version because that's what the web3_clientVersion jsonrpc call returns // https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion clientAndVersion string @@ -29,20 +29,24 @@ type InstrumentedClient struct { var _ EthClient = (*InstrumentedClient)(nil) -func NewInstrumentedClient(client *ethclient.Client, metrics metrics.Metrics) *InstrumentedClient { +func NewInstrumentedClient(rpcAddress string, rpcCallsCollector *rpccalls.Collector) (*InstrumentedClient, error) { + client, err := ethclient.Dial(rpcAddress) + if err != nil { + return nil, err + } clientAndVersion := getClientAndVersion(client) return &InstrumentedClient{ - client: client, - metrics: metrics, - clientAndVersion: clientAndVersion, - } + client: client, + rpcCallsCollector: rpcCallsCollector, + clientAndVersion: clientAndVersion, + }, nil } // gethClient interface methods func (iec *InstrumentedClient) ChainID(ctx context.Context) (*big.Int, error) { chainID := func() (*big.Int, error) { return iec.client.ChainID(ctx) } - id, err := instrumentFunction[*big.Int](chainID, "eth_chainId", iec.metrics, iec.clientAndVersion) + id, err := instrumentFunction[*big.Int](chainID, "eth_chainId", iec) return id, err } @@ -52,7 +56,7 @@ func (iec *InstrumentedClient) BalanceAt( blockNumber *big.Int, ) (*big.Int, error) { balanceAt := func() (*big.Int, error) { return iec.client.BalanceAt(ctx, account, blockNumber) } - balance, err := instrumentFunction[*big.Int](balanceAt, "eth_getBalance", iec.metrics, iec.clientAndVersion) + balance, err := instrumentFunction[*big.Int](balanceAt, "eth_getBalance", iec) if err != nil { return nil, err } @@ -61,7 +65,7 @@ func (iec *InstrumentedClient) BalanceAt( func (iec *InstrumentedClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { blockByHash := func() (*types.Block, error) { return iec.client.BlockByHash(ctx, hash) } - block, err := instrumentFunction[*types.Block](blockByHash, "eth_getBlockByHash", iec.metrics, iec.clientAndVersion) + block, err := instrumentFunction[*types.Block](blockByHash, "eth_getBlockByHash", iec) if err != nil { return nil, err } @@ -73,8 +77,7 @@ func (iec *InstrumentedClient) BlockByNumber(ctx context.Context, number *big.In block, err := instrumentFunction[*types.Block]( blockByNumber, "eth_getBlockByNumber", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -84,7 +87,7 @@ func (iec *InstrumentedClient) BlockByNumber(ctx context.Context, number *big.In func (iec *InstrumentedClient) BlockNumber(ctx context.Context) (uint64, error) { blockNumber := func() (uint64, error) { return iec.client.BlockNumber(ctx) } - number, err := instrumentFunction[uint64](blockNumber, "eth_blockNumber", iec.metrics, iec.clientAndVersion) + number, err := instrumentFunction[uint64](blockNumber, "eth_blockNumber", iec) if err != nil { return 0, err } @@ -97,7 +100,7 @@ func (iec *InstrumentedClient) CallContract( blockNumber *big.Int, ) ([]byte, error) { callContract := func() ([]byte, error) { return iec.client.CallContract(ctx, call, blockNumber) } - bytes, err := instrumentFunction[[]byte](callContract, "eth_call", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](callContract, "eth_call", iec) if err != nil { return nil, err } @@ -110,7 +113,7 @@ func (iec *InstrumentedClient) CallContractAtHash( blockHash common.Hash, ) ([]byte, error) { callContractAtHash := func() ([]byte, error) { return iec.client.CallContractAtHash(ctx, msg, blockHash) } - bytes, err := instrumentFunction[[]byte](callContractAtHash, "eth_call", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](callContractAtHash, "eth_call", iec) if err != nil { return nil, err } @@ -123,7 +126,7 @@ func (iec *InstrumentedClient) CodeAt( blockNumber *big.Int, ) ([]byte, error) { call := func() ([]byte, error) { return iec.client.CodeAt(ctx, contract, blockNumber) } - bytes, err := instrumentFunction[[]byte](call, "eth_getCode", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](call, "eth_getCode", iec) if err != nil { return nil, err } @@ -132,7 +135,7 @@ func (iec *InstrumentedClient) CodeAt( func (iec *InstrumentedClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { estimateGas := func() (uint64, error) { return iec.client.EstimateGas(ctx, call) } - gas, err := instrumentFunction[uint64](estimateGas, "eth_estimateGas", iec.metrics, iec.clientAndVersion) + gas, err := instrumentFunction[uint64](estimateGas, "eth_estimateGas", iec) if err != nil { return 0, err } @@ -151,8 +154,7 @@ func (iec *InstrumentedClient) FeeHistory( history, err := instrumentFunction[*ethereum.FeeHistory]( feeHistory, "eth_feeHistory", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -162,7 +164,7 @@ func (iec *InstrumentedClient) FeeHistory( func (iec *InstrumentedClient) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { filterLogs := func() ([]types.Log, error) { return iec.client.FilterLogs(ctx, query) } - logs, err := instrumentFunction[[]types.Log](filterLogs, "eth_getLogs", iec.metrics, iec.clientAndVersion) + logs, err := instrumentFunction[[]types.Log](filterLogs, "eth_getLogs", iec) if err != nil { return nil, err } @@ -174,8 +176,7 @@ func (iec *InstrumentedClient) HeaderByHash(ctx context.Context, hash common.Has header, err := instrumentFunction[*types.Header]( headerByHash, "eth_getBlockByHash", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -188,8 +189,7 @@ func (iec *InstrumentedClient) HeaderByNumber(ctx context.Context, number *big.I header, err := instrumentFunction[*types.Header]( headerByNumber, "eth_getBlockByNumber", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -199,7 +199,7 @@ func (iec *InstrumentedClient) HeaderByNumber(ctx context.Context, number *big.I func (iec *InstrumentedClient) NetworkID(ctx context.Context) (*big.Int, error) { networkID := func() (*big.Int, error) { return iec.client.NetworkID(ctx) } - id, err := instrumentFunction[*big.Int](networkID, "net_version", iec.metrics, iec.clientAndVersion) + id, err := instrumentFunction[*big.Int](networkID, "net_version", iec) if err != nil { return nil, err } @@ -212,7 +212,7 @@ func (iec *InstrumentedClient) NonceAt( blockNumber *big.Int, ) (uint64, error) { nonceAt := func() (uint64, error) { return iec.client.NonceAt(ctx, account, blockNumber) } - nonce, err := instrumentFunction[uint64](nonceAt, "eth_getTransactionCount", iec.metrics, iec.clientAndVersion) + nonce, err := instrumentFunction[uint64](nonceAt, "eth_getTransactionCount", iec) if err != nil { return 0, err } @@ -221,7 +221,7 @@ func (iec *InstrumentedClient) NonceAt( func (iec *InstrumentedClient) PeerCount(ctx context.Context) (uint64, error) { peerCount := func() (uint64, error) { return iec.client.PeerCount(ctx) } - count, err := instrumentFunction[uint64](peerCount, "net_peerCount", iec.metrics, iec.clientAndVersion) + count, err := instrumentFunction[uint64](peerCount, "net_peerCount", iec) if err != nil { return 0, err } @@ -230,7 +230,7 @@ func (iec *InstrumentedClient) PeerCount(ctx context.Context) (uint64, error) { func (iec *InstrumentedClient) PendingBalanceAt(ctx context.Context, account common.Address) (*big.Int, error) { pendingBalanceAt := func() (*big.Int, error) { return iec.client.PendingBalanceAt(ctx, account) } - balance, err := instrumentFunction[*big.Int](pendingBalanceAt, "eth_getBalance", iec.metrics, iec.clientAndVersion) + balance, err := instrumentFunction[*big.Int](pendingBalanceAt, "eth_getBalance", iec) if err != nil { return nil, err } @@ -239,7 +239,7 @@ func (iec *InstrumentedClient) PendingBalanceAt(ctx context.Context, account com func (iec *InstrumentedClient) PendingCallContract(ctx context.Context, call ethereum.CallMsg) ([]byte, error) { pendingCallContract := func() ([]byte, error) { return iec.client.PendingCallContract(ctx, call) } - bytes, err := instrumentFunction[[]byte](pendingCallContract, "eth_call", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](pendingCallContract, "eth_call", iec) if err != nil { return nil, err } @@ -248,7 +248,7 @@ func (iec *InstrumentedClient) PendingCallContract(ctx context.Context, call eth func (iec *InstrumentedClient) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { pendingCodeAt := func() ([]byte, error) { return iec.client.PendingCodeAt(ctx, account) } - bytes, err := instrumentFunction[[]byte](pendingCodeAt, "eth_getCode", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](pendingCodeAt, "eth_getCode", iec) if err != nil { return nil, err } @@ -260,8 +260,7 @@ func (iec *InstrumentedClient) PendingNonceAt(ctx context.Context, account commo nonce, err := instrumentFunction[uint64]( pendingNonceAt, "eth_getTransactionCount", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return 0, err @@ -275,7 +274,7 @@ func (iec *InstrumentedClient) PendingStorageAt( key common.Hash, ) ([]byte, error) { pendingStorageAt := func() ([]byte, error) { return iec.client.PendingStorageAt(ctx, account, key) } - bytes, err := instrumentFunction[[]byte](pendingStorageAt, "eth_getStorageAt", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](pendingStorageAt, "eth_getStorageAt", iec) if err != nil { return nil, err } @@ -287,8 +286,7 @@ func (iec *InstrumentedClient) PendingTransactionCount(ctx context.Context) (uin count, err := instrumentFunction[uint]( pendingTransactionCount, "eth_getBlockTransactionCountByNumber", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return 0, err @@ -301,7 +299,7 @@ func (iec *InstrumentedClient) SendTransaction(ctx context.Context, tx *types.Tr // so we just wrap the SendTransaction method in a function that returns 0 as its value, // which we throw out below sendTransaction := func() (int, error) { return 0, iec.client.SendTransaction(ctx, tx) } - _, err := instrumentFunction[int](sendTransaction, "eth_sendRawTransaction", iec.metrics, iec.clientAndVersion) + _, err := instrumentFunction[int](sendTransaction, "eth_sendRawTransaction", iec) return err } @@ -312,7 +310,7 @@ func (iec *InstrumentedClient) StorageAt( blockNumber *big.Int, ) ([]byte, error) { storageAt := func() ([]byte, error) { return iec.client.StorageAt(ctx, account, key, blockNumber) } - bytes, err := instrumentFunction[[]byte](storageAt, "eth_getStorageAt", iec.metrics, iec.clientAndVersion) + bytes, err := instrumentFunction[[]byte](storageAt, "eth_getStorageAt", iec) if err != nil { return nil, err } @@ -328,8 +326,7 @@ func (iec *InstrumentedClient) SubscribeFilterLogs( subscription, err := instrumentFunction[ethereum.Subscription]( subscribeFilterLogs, "eth_subscribe", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -345,8 +342,7 @@ func (iec *InstrumentedClient) SubscribeNewHead( subscription, err := instrumentFunction[ethereum.Subscription]( subscribeNewHead, "eth_subscribe", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -356,7 +352,7 @@ func (iec *InstrumentedClient) SubscribeNewHead( func (iec *InstrumentedClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { suggestGasPrice := func() (*big.Int, error) { return iec.client.SuggestGasPrice(ctx) } - gasPrice, err := instrumentFunction[*big.Int](suggestGasPrice, "eth_gasPrice", iec.metrics, iec.clientAndVersion) + gasPrice, err := instrumentFunction[*big.Int](suggestGasPrice, "eth_gasPrice", iec) if err != nil { return nil, err } @@ -368,8 +364,7 @@ func (iec *InstrumentedClient) SuggestGasTipCap(ctx context.Context) (*big.Int, gasTipCap, err := instrumentFunction[*big.Int]( suggestGasTipCap, "eth_maxPriorityFeePerGas", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -382,8 +377,7 @@ func (iec *InstrumentedClient) SyncProgress(ctx context.Context) (*ethereum.Sync progress, err := instrumentFunction[*ethereum.SyncProgress]( syncProgress, "eth_syncing", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -400,17 +394,16 @@ func (iec *InstrumentedClient) TransactionByHash( start := time.Now() tx, isPending, err = iec.client.TransactionByHash(ctx, hash) // we count both successful and erroring calls (even though this is not well defined in the spec) - iec.metrics.AddRPCRequestTotal("eth_getTransactionByHash", iec.clientAndVersion, iec.clientAndVersion) + iec.rpcCallsCollector.AddRPCRequestTotal("eth_getTransactionByHash", iec.clientAndVersion) if err != nil { return nil, false, err } rpcRequestDuration := time.Since(start) // we only observe the duration of successful calls (even though this is not well defined in the spec) - iec.metrics.ObserveRPCRequestDurationSeconds( + iec.rpcCallsCollector.ObserveRPCRequestDurationSeconds( float64(rpcRequestDuration), "eth_getTransactionByHash", iec.clientAndVersion, - iec.clientAndVersion, ) return tx, isPending, nil @@ -421,8 +414,7 @@ func (iec *InstrumentedClient) TransactionCount(ctx context.Context, blockHash c count, err := instrumentFunction[uint]( transactionCount, "eth_getBlockTransactionCountByHash", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return 0, err @@ -439,8 +431,7 @@ func (iec *InstrumentedClient) TransactionInBlock( tx, err := instrumentFunction[*types.Transaction]( transactionInBlock, "eth_getTransactionByBlockHashAndIndex", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -453,8 +444,7 @@ func (iec *InstrumentedClient) TransactionReceipt(ctx context.Context, txHash co receipt, err := instrumentFunction[*types.Receipt]( transactionReceipt, "eth_getTransactionReceipt", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return nil, err @@ -472,8 +462,7 @@ func (iec *InstrumentedClient) TransactionSender( address, err := instrumentFunction[common.Address]( transactionSender, "eth_getSender", - iec.metrics, - iec.clientAndVersion, + iec, ) if err != nil { return common.Address{}, err @@ -515,23 +504,21 @@ func getClientAndVersion(client *ethclient.Client) string { func instrumentFunction[T any]( rpcCall func() (T, error), rpcMethodName string, - metrics metrics.Metrics, - clientAndVersion string, + iec *InstrumentedClient, ) (value T, err error) { start := time.Now() result, err := rpcCall() // we count both successful and erroring calls (even though this is not well defined in the spec) - metrics.AddRPCRequestTotal(rpcMethodName, clientAndVersion, clientAndVersion) + iec.rpcCallsCollector.AddRPCRequestTotal(rpcMethodName, iec.clientAndVersion) if err != nil { return value, err } rpcRequestDuration := time.Since(start) // we only observe the duration of successful calls (even though this is not well defined in the spec) - metrics.ObserveRPCRequestDurationSeconds( + iec.rpcCallsCollector.ObserveRPCRequestDurationSeconds( float64(rpcRequestDuration), rpcMethodName, - clientAndVersion, - clientAndVersion, + iec.clientAndVersion, ) return result, nil } diff --git a/chainio/constructor/constructor.go b/chainio/constructor/constructor.go index 21617f2b..71323cf5 100644 --- a/chainio/constructor/constructor.go +++ b/chainio/constructor/constructor.go @@ -28,6 +28,11 @@ type Config struct { PromMetricsIpPortAddress string `yaml:"prometheus_metrics_ip_port_address"` } +// TODO: this is confusing right now because clients are not instrumented clients, but +// we return metrics and prometheus reg, so user has to build instrumented clients at the call +// site if they need them. We should probably separate into two separate constructors, one +// for non-instrumented clients that doesn't return metrics/reg, and another instrumented-constructor +// that returns instrumented clients and the metrics/reg. type Clients struct { AvsRegistryChainReader avsregistry.AvsRegistryReader AvsRegistryChainWriter avsregistry.AvsRegistryWriter diff --git a/metrics/collectors/economic.go b/metrics/collectors/economic/economic.go similarity index 94% rename from metrics/collectors/economic.go rename to metrics/collectors/economic/economic.go index bce177ed..1103130f 100644 --- a/metrics/collectors/economic.go +++ b/metrics/collectors/economic/economic.go @@ -1,5 +1,5 @@ // Package collectors contains custom prometheus collectors that are not just simple instrumented metrics -package collectors +package economic import ( "context" @@ -13,15 +13,15 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -// EconomicCollector to export the economic metrics listed at +// Collector exports the economic metrics listed at // // https://eigen.nethermind.io/docs/spec/metrics/metrics-examples#economics-metrics // // these are metrics that are exported not via instrumentation, but instead by proxying // a call to the relevant eigenlayer contracts -// EconomicCollector should be registered in the same prometheus registry that is passed to metrics.NewEigenMetrics +// Collector should be registered in the same prometheus registry that is passed to metrics.NewEigenMetrics // so that they are exported on the same port -type EconomicCollector struct { +type Collector struct { // TODO(samlaf): we use a chain as the backend for now, but should eventually move to a subgraph elReader elcontracts.ELReader avsRegistryReader avsregistry.AvsRegistryReader @@ -48,7 +48,7 @@ type EconomicCollector struct { // then the operator's registeredStake will remain 600 until updateStakes() is called, at which point it will // drop to the correct value of 300. registeredStake *prometheus.Desc - + // TODO(samlaf): Removing this as not part of avs node spec anymore. // delegatedShares is the total shares delegated to the operator in each strategy // strategies are currently token specific, and we have one for cbETH, rETH, and stETH, @@ -57,18 +57,18 @@ type EconomicCollector struct { // delegatedShares *prometheus.Desc } -var _ prometheus.Collector = (*EconomicCollector)(nil) +var _ prometheus.Collector = (*Collector)(nil) -func NewEconomicCollector( +func NewCollector( elReader elcontracts.ELReader, avsRegistryReader avsregistry.AvsRegistryReader, avsName string, logger logging.Logger, operatorAddr common.Address, quorumNames map[types.QuorumNum]string, -) *EconomicCollector { +) *Collector { operatorId, err := avsRegistryReader.GetOperatorId(context.Background(), operatorAddr) if err != nil { logger.Error("Failed to get operator id", "err", err) } - return &EconomicCollector{ + return &Collector{ elReader: elReader, avsRegistryReader: avsRegistryReader, logger: logger, @@ -104,7 +104,7 @@ func NewEconomicCollector( // Describe is implemented with DescribeByCollect. That's possible because the // Collect method will always return the same two metrics with the same two // descriptors. -func (ec *EconomicCollector) Describe(ch chan<- *prometheus.Desc) { +func (ec *Collector) Describe(ch chan<- *prometheus.Desc) { ch <- ec.slashingStatus ch <- ec.registeredStake // ch <- ec.delegatedShares @@ -114,7 +114,7 @@ func (ec *EconomicCollector) Describe(ch chan<- *prometheus.Desc) { // see https://github.com/prometheus/client_golang/blob/v1.16.0/prometheus/collector.go#L27 // collect just makes jsonrpc calls to the slasher and delegationManager and then creates // constant metrics with the results -func (ec *EconomicCollector) Collect(ch chan<- prometheus.Metric) { +func (ec *Collector) Collect(ch chan<- prometheus.Metric) { // collect slashingStatus metric // TODO(samlaf): note that this call is not avs specific, so every avs will have the same value if the operator has been slashed // if we want instead to only output 1 if the operator has been slashed for a specific avs, we have 2 choices: diff --git a/metrics/collectors/economic_test.go b/metrics/collectors/economic/economic_test.go similarity index 93% rename from metrics/collectors/economic_test.go rename to metrics/collectors/economic/economic_test.go index 2769bad5..7ca8d80f 100644 --- a/metrics/collectors/economic_test.go +++ b/metrics/collectors/economic/economic_test.go @@ -1,4 +1,4 @@ -package collectors +package economic import ( "math/big" @@ -53,7 +53,7 @@ func TestEconomicCollector(t *testing.T) { avsRegistryReader.EXPECT().GetOperatorId(gomock.Any(), operatorAddr).Return(operatorId, nil) logger := logging.NewNoopLogger() - economicCollector := NewEconomicCollector(ethReader, avsRegistryReader, "testavs", logger, operatorAddr, quorumNames) + economicCollector := NewCollector(ethReader, avsRegistryReader, "testavs", logger, operatorAddr, quorumNames) count := testutil.CollectAndCount(economicCollector, "eigen_slashing_status", "eigen_registered_stakes") // 1 for eigen_slashing_status, and 2 for eigen_registered_stakes (1 per quorum) diff --git a/metrics/collectors/rpc_calls/rpc_calls.go b/metrics/collectors/rpc_calls/rpc_calls.go new file mode 100644 index 00000000..a6a72423 --- /dev/null +++ b/metrics/collectors/rpc_calls/rpc_calls.go @@ -0,0 +1,58 @@ +package rpccalls + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// Collector contains instrumented metrics that should be incremented by the avs node using the methods below +// it is used by the eigensdk's instrumented_client, but can also be used by avs teams to instrument their own client +// if it differs from ours. +type Collector struct { + rpcRequestDurationSeconds *prometheus.HistogramVec + rpcRequestTotal *prometheus.CounterVec +} + +// NewCollector returns an rpccalls Collector that collects metrics for json-rpc calls +func NewCollector(eigenNamespace, avsName string, reg prometheus.Registerer) *Collector { + return &Collector{ + rpcRequestDurationSeconds: promauto.With(reg).NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: eigenNamespace, + Name: "rpc_request_duration_seconds", + Help: "Duration of json-rpc in seconds", + ConstLabels: prometheus.Labels{"avs_name": avsName}, + }, + // client_version is the client name and its current version. We don't separate them because + // this string is returned as a single string by the web3_clientVersion jsonrpc call + // which doesn't follow any standardized format so not possible to parse it... + // https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion + []string{"method", "client_version"}, + ), + rpcRequestTotal: promauto.With(reg).NewCounterVec( + prometheus.CounterOpts{ + Namespace: eigenNamespace, + Name: "rpc_request_total", + Help: "Total number of json-rpc requests", + ConstLabels: prometheus.Labels{"avs_name": avsName}, + }, + []string{"method", "client_version"}, + ), + } +} + +// ObserveRPCRequestDurationSeconds observes the duration of a json-rpc request +func (c *Collector) ObserveRPCRequestDurationSeconds(duration float64, method, clientVersion string) { + c.rpcRequestDurationSeconds.With(prometheus.Labels{ + "method": method, + "client_version": clientVersion, + }).Observe(duration) +} + +// AddRPCRequestTotal adds a json-rpc request to the total number of requests +func (c *Collector) AddRPCRequestTotal(method, clientVersion string) { + c.rpcRequestTotal.With(prometheus.Labels{ + "method": method, + "client_version": clientVersion, + }).Inc() +} diff --git a/metrics/collectors/rpc_calls/rpc_calls_test.go b/metrics/collectors/rpc_calls/rpc_calls_test.go new file mode 100644 index 00000000..10c9bf70 --- /dev/null +++ b/metrics/collectors/rpc_calls/rpc_calls_test.go @@ -0,0 +1,22 @@ +package rpccalls + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" +) + +func TestRpcCallsCollector(t *testing.T) { + reg := prometheus.NewRegistry() + rpcCallsCollector := NewCollector("testavs", "testname", reg) + + rpcCallsCollector.ObserveRPCRequestDurationSeconds(1, "testmethod", "testclient/testversion") + // TODO(samlaf): not sure how to test histogram values.. there's no mention of histograms in + // https://pkg.go.dev/github.com/prometheus/client_golang/prometheus/testutil + // assert.Equal(t, 1.0, testutil.ToFloat64(suite.metrics.rpcRequestDurationSeconds)) + + rpcCallsCollector.AddRPCRequestTotal("testmethod", "testclient/testversion") + assert.Equal(t, 1.0, testutil.ToFloat64(rpcCallsCollector.rpcRequestTotal.WithLabelValues("testmethod", "testclient/testversion"))) +} diff --git a/metrics/eigenmetrics.go b/metrics/eigenmetrics.go index 496e8fa9..de8e567a 100644 --- a/metrics/eigenmetrics.go +++ b/metrics/eigenmetrics.go @@ -20,8 +20,6 @@ type EigenMetrics struct { // fees are not yet turned on, so these should just be 0 for the time being feeEarnedTotal *prometheus.CounterVec performanceScore prometheus.Gauge - rpcRequestDurationSeconds *prometheus.HistogramVec - rpcRequestTotal *prometheus.CounterVec } var _ Metrics = (*EigenMetrics)(nil) @@ -51,24 +49,6 @@ func NewEigenMetrics(avsName, ipPortAddress string, reg prometheus.Registerer, l ConstLabels: prometheus.Labels{"avs_name": avsName}, }, ), - rpcRequestDurationSeconds: promauto.With(reg).NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: eigenNamespace, - Name: "rpc_request_duration_seconds", - Help: "Duration of json-rpc in seconds", - ConstLabels: prometheus.Labels{"avs_name": avsName}, - }, - []string{"method", "client", "version"}, - ), - rpcRequestTotal: promauto.With(reg).NewCounterVec( - prometheus.CounterOpts{ - Namespace: eigenNamespace, - Name: "rpc_request_total", - Help: "Total number of json-rpc requests", - ConstLabels: prometheus.Labels{"avs_name": avsName}, - }, - []string{"method", "client", "version"}, - ), ipPortAddress: ipPortAddress, logger: logger, } @@ -95,38 +75,6 @@ func (m *EigenMetrics) SetPerformanceScore(score float64) { m.performanceScore.Set(score) } -// ObserveRPCRequestDurationSeconds observes the duration of a json-rpc request -func (m *EigenMetrics) ObserveRPCRequestDurationSeconds(duration float64, method, client, version string) { - // TODO(samlaf): client and version are separate because we're following the current avs-node-spec - // https://eigen.nethermind.io/docs/metrics/metrics-prom-spec - // but the web3_clientVersion jsonrpc call returns a single string that has both - // in a non-standardized format so not possible to parse... - // https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion - // for now we'll just duplicate the clientVersion string in both client and version, - // but we should eventually have a single clientVersion label - m.rpcRequestDurationSeconds.With(prometheus.Labels{ - "method": method, - "client": client, - "version": version, - }).Observe(duration) -} - -// AddRPCRequestTotal adds a json-rpc request to the total number of requests -func (m *EigenMetrics) AddRPCRequestTotal(method, client, version string) { - // TODO(samlaf): client and version are separate because we're following the current avs-node-spec - // https://eigen.nethermind.io/docs/metrics/metrics-prom-spec - // but the web3_clientVersion jsonrpc call returns a single string that has both - // in a non-standardized format so not possible to parse... - // https://ethereum.org/en/developers/docs/apis/json-rpc/#web3_clientversion - // for now we'll just duplicate the clientVersion string in both client and version, - // but we should eventually have a single clientVersion label - m.rpcRequestTotal.With(prometheus.Labels{ - "method": method, - "client": client, - "version": version, - }).Inc() -} - // Start creates an http handler for reg and starts the prometheus server in a goroutine, listening at m.ipPortAddress. // reg needs to be the prometheus registry that was passed in the NewEigenMetrics constructor func (m *EigenMetrics) Start(ctx context.Context, reg prometheus.Gatherer) <-chan error { diff --git a/metrics/eigenmetrics_example_test.go b/metrics/eigenmetrics_example_test.go index d5779613..18baa441 100644 --- a/metrics/eigenmetrics_example_test.go +++ b/metrics/eigenmetrics_example_test.go @@ -9,11 +9,12 @@ import ( "github.com/Layr-Labs/eigensdk-go/chainio/avsregistry" sdkclients "github.com/Layr-Labs/eigensdk-go/chainio/clients" - ethclients "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" "github.com/Layr-Labs/eigensdk-go/chainio/elcontracts" "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/metrics" - "github.com/Layr-Labs/eigensdk-go/metrics/collectors" + "github.com/Layr-Labs/eigensdk-go/metrics/collectors/economic" + rpccalls "github.com/Layr-Labs/eigensdk-go/metrics/collectors/rpc_calls" "github.com/Layr-Labs/eigensdk-go/types" "github.com/ethereum/go-ethereum/common" "github.com/prometheus/client_golang/prometheus" @@ -35,11 +36,11 @@ func ExampleEigenMetrics() { slasherAddr := common.HexToAddress("0x0") blsPubKeyCompendiumAddr := common.HexToAddress("0x0") - ethHttpClient, err := ethclients.NewClient("http://localhost:8545") + ethHttpClient, err := eth.NewClient("http://localhost:8545") if err != nil { panic(err) } - ethWsClient, err := ethclients.NewClient("ws://localhost:8545") + ethWsClient, err := eth.NewClient("ws://localhost:8545") if err != nil { panic(err) } @@ -74,8 +75,17 @@ func ExampleEigenMetrics() { } // We must register the economic metrics separately because they are exported metrics (from jsonrpc or subgraph calls) // and not instrumented metrics: see https://prometheus.io/docs/instrumenting/writing_clientlibs/#overall-structure - economicMetricsCollector := collectors.NewEconomicCollector(eigenlayerReader, avsRegistryReader, "exampleAvs", logger, operatorAddr, quorumNames) + economicMetricsCollector := economic.NewCollector(eigenlayerReader, avsRegistryReader, "exampleAvs", logger, operatorAddr, quorumNames) reg.MustRegister(economicMetricsCollector) + rpcCallsCollector := rpccalls.NewCollector("eigen", "exampleAvs", reg) + instrumentedEthClient, err := eth.NewInstrumentedClient("http://localhost:8545", rpcCallsCollector) + if err != nil { + panic(err) + } + eigenMetrics.Start(context.Background(), reg) + + // use instrumentedEthClient as you would a normal ethClient + _ = instrumentedEthClient } diff --git a/metrics/eigenmetrics_test.go b/metrics/eigenmetrics_test.go index 65c7e63c..cfd4d301 100644 --- a/metrics/eigenmetrics_test.go +++ b/metrics/eigenmetrics_test.go @@ -44,8 +44,6 @@ func (suite *MetricsTestSuite) TestEigenMetricsServerIntegration() { suite.metrics.AddFeeEarnedTotal(1, "testtoken") suite.metrics.SetPerformanceScore(1) - suite.metrics.ObserveRPCRequestDurationSeconds(1, "testmethod", "testclient", "testversion") - suite.metrics.AddRPCRequestTotal("testmethod", "testclient", "testversion") resp, err := http.Get("http://localhost:9090/metrics") assert.NoError(suite.T(), err) @@ -57,8 +55,6 @@ func (suite *MetricsTestSuite) TestEigenMetricsServerIntegration() { // the other metrics have labels (they are vectors) so they don't appear in the output unless we use them once at least assert.Contains(suite.T(), string(body), "eigen_fees_earned_total") assert.Contains(suite.T(), string(body), "eigen_performance_score") - assert.Contains(suite.T(), string(body), "eigen_rpc_request_duration_seconds") - assert.Contains(suite.T(), string(body), "eigen_rpc_request_total") } // The below tests are very pedantic but at least show how avs teams can make use of @@ -75,18 +71,6 @@ func (suite *MetricsTestSuite) TestSetEigenPerformanceScore() { assert.Equal(suite.T(), 1.0, testutil.ToFloat64(suite.metrics.performanceScore)) } -func (suite *MetricsTestSuite) TestObserveEigenRPCRequestDurationSeconds() { - suite.metrics.ObserveRPCRequestDurationSeconds(1, "testmethod", "testclient", "testversion") - // TODO(samlaf): not sure how to test histogram values.. there's no mention of histograms in - // https://pkg.go.dev/github.com/prometheus/client_golang/prometheus/testutil - // assert.Equal(t, 1.0, testutil.ToFloat64(suite.metrics.rpcRequestDurationSeconds)) -} - -func (suite *MetricsTestSuite) TestAddEigenRPCRequestTotal() { - suite.metrics.AddRPCRequestTotal("testmethod", "testclient", "testversion") - assert.Equal(suite.T(), 1.0, testutil.ToFloat64(suite.metrics.rpcRequestTotal.WithLabelValues("testmethod", "testclient", "testversion"))) -} - // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run func TestMetricsTestSuite(t *testing.T) { diff --git a/metrics/metrics.go b/metrics/metrics.go index f722c72a..672bb896 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -6,10 +6,12 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +// Metrics is the interface for the EigenMetrics server +// it only wraps 2 of the 6 methods required by the spec (https://eigen.nethermind.io/docs/spec/metrics/metrics-prom-spec) +// the others are implemented by the economics and rpc_calls collectors, +// and need to be registered with the metrics server prometheus registry type Metrics interface { AddFeeEarnedTotal(amount float64, token string) SetPerformanceScore(score float64) - ObserveRPCRequestDurationSeconds(duration float64, method, client, version string) - AddRPCRequestTotal(method, client, version string) Start(ctx context.Context, reg prometheus.Gatherer) <-chan error } diff --git a/metrics/noop_metrics.go b/metrics/noop_metrics.go index 7f9df4ec..9f1fd36a 100644 --- a/metrics/noop_metrics.go +++ b/metrics/noop_metrics.go @@ -17,9 +17,6 @@ func NewNoopMetrics() *NoopMetrics { func (m *NoopMetrics) AddFeeEarnedTotal(amount float64, token string) {} func (m *NoopMetrics) SetPerformanceScore(score float64) {} -func (m *NoopMetrics) ObserveRPCRequestDurationSeconds(duration float64, method, client, version string) { -} -func (m *NoopMetrics) AddRPCRequestTotal(method, client, version string) {} func (m *NoopMetrics) Start(ctx context.Context, reg prometheus.Gatherer) <-chan error { return nil }