diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index e1c062be..c7ef87a2 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -154,6 +154,10 @@ func init() { BlockChainFactories["Base Archive"] = base.NewBaseRPC } +type metricsSetter interface { + SetMetrics(*common.Metrics) +} + // NewBlockChain creates bchain.BlockChain and bchain.Mempool for the coin passed by the parameter coin func NewBlockChain(coin string, configfile string, pushHandler func(bchain.NotificationType), metrics *common.Metrics) (bchain.BlockChain, bchain.Mempool, error) { data, err := os.ReadFile(configfile) @@ -173,6 +177,9 @@ func NewBlockChain(coin string, configfile string, pushHandler func(bchain.Notif if err != nil { return nil, nil, err } + if withMetrics, ok := bc.(metricsSetter); ok { + withMetrics.SetMetrics(metrics) + } err = bc.Initialize() if err != nil { return nil, nil, err diff --git a/bchain/coins/eth/contract.go b/bchain/coins/eth/contract.go index 8df7e7fe..9d275697 100644 --- a/bchain/coins/eth/contract.go +++ b/bchain/coins/eth/contract.go @@ -290,12 +290,14 @@ func (b *EthereumRPC) EthereumTypeRpcCallAtBlock(data, to, from string, blockNum args["from"] = from } + b.observeEthCall("single", 1) ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var r string blockArg := bchain.ToBlockNumArg(blockNumber) err := b.RPC.CallContext(ctx, &r, "eth_call", args, blockArg) if err != nil { + b.observeEthCallError("single", "rpc") return "", err } return r, nil @@ -373,6 +375,7 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractBalanceAtBlock(addrDesc, contr } r := parseSimpleNumericProperty(data) if r == nil { + b.observeEthCallError("single", "invalid") return nil, errors.New("Invalid balance") } return r, nil @@ -445,14 +448,18 @@ func (b *EthereumRPC) erc20BalancesBatchAtBlock(batcher batchCaller, callData st Result: &results[i], } } + b.observeEthCall("batch", len(contractDescs)) + b.observeEthCallBatch(len(contractDescs)) ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() if err := batcher.BatchCallContext(ctx, batch); err != nil { + b.observeEthCallError("batch", "rpc") return nil, err } balances := make([]*big.Int, len(contractDescs)) for i := range batch { if batch[i].Error != nil { + b.observeEthCallError("batch", "elem") glog.Warningf("erc20 batch eth_call failed for %s: %v", hexutil.Encode(contractDescs[i]), batch[i].Error) // In case of batch failure, retry missing/failed elements as single calls. data, err := b.EthereumTypeRpcCallAtBlock(callData, hexutil.Encode(contractDescs[i]), "", blockNumber) @@ -462,6 +469,7 @@ func (b *EthereumRPC) erc20BalancesBatchAtBlock(batcher batchCaller, callData st } balances[i] = parseSimpleNumericProperty(data) if balances[i] == nil { + b.observeEthCallError("single", "invalid") glog.Warningf("erc20 single eth_call invalid result for %s: %q", hexutil.Encode(contractDescs[i]), data) } continue @@ -469,6 +477,7 @@ func (b *EthereumRPC) erc20BalancesBatchAtBlock(batcher batchCaller, callData st // Leave nil on parse failures so callers can retry per contract if needed. balances[i] = parseSimpleNumericProperty(results[i]) if balances[i] == nil { + b.observeEthCallError("batch", "invalid") glog.Warningf("erc20 batch eth_call invalid result for %s: %q", hexutil.Encode(contractDescs[i]), results[i]) } } diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index 0b9bffcb..f6efd793 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -92,6 +92,7 @@ type EthereumRPC struct { NewTx bchain.EVMNewTxSubscriber newTxSubscription bchain.EVMClientSubscription ChainConfig *Configuration + metrics *common.Metrics supportedStakingPools []string stakingPoolNames []string stakingPoolContracts []string @@ -161,6 +162,31 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification return s, nil } +func (b *EthereumRPC) SetMetrics(metrics *common.Metrics) { + b.metrics = metrics +} + +func (b *EthereumRPC) observeEthCall(mode string, count int) { + if b.metrics == nil || count <= 0 { + return + } + b.metrics.EthCallRequests.With(common.Labels{"mode": mode}).Add(float64(count)) +} + +func (b *EthereumRPC) observeEthCallError(mode, errType string) { + if b.metrics == nil { + return + } + b.metrics.EthCallErrors.With(common.Labels{"mode": mode, "type": errType}).Inc() +} + +func (b *EthereumRPC) observeEthCallBatch(size int) { + if b.metrics == nil || size <= 0 { + return + } + b.metrics.EthCallBatchSize.Observe(float64(size)) +} + // EnsureSameRPCHost validates that both RPC URLs point to the same host. func EnsureSameRPCHost(httpURL, wsURL string) error { if httpURL == "" || wsURL == "" { diff --git a/common/metrics.go b/common/metrics.go index 922364fb..6cef6ebb 100644 --- a/common/metrics.go +++ b/common/metrics.go @@ -20,6 +20,9 @@ type Metrics struct { MempoolResyncDuration prometheus.Histogram TxCacheEfficiency *prometheus.CounterVec RPCLatency *prometheus.HistogramVec + EthCallRequests *prometheus.CounterVec + EthCallErrors *prometheus.CounterVec + EthCallBatchSize prometheus.Histogram IndexResyncErrors *prometheus.CounterVec IndexDBSize prometheus.Gauge ExplorerViews *prometheus.CounterVec @@ -149,6 +152,30 @@ func GetMetrics(coin string) (*Metrics, error) { }, []string{"method", "error"}, ) + metrics.EthCallRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "blockbook_eth_call_requests", + Help: "Total number of eth_call requests by mode", + ConstLabels: Labels{"coin": coin}, + }, + []string{"mode"}, + ) + metrics.EthCallErrors = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "blockbook_eth_call_errors", + Help: "Total number of eth_call errors by mode and type", + ConstLabels: Labels{"coin": coin}, + }, + []string{"mode", "type"}, + ) + metrics.EthCallBatchSize = prometheus.NewHistogram( + prometheus.HistogramOpts{ + Name: "blockbook_eth_call_batch_size", + Help: "Number of eth_call items per batch request", + Buckets: []float64{1, 2, 5, 10, 20, 50, 100, 200}, + ConstLabels: Labels{"coin": coin}, + }, + ) metrics.IndexResyncErrors = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "blockbook_index_resync_errors",