diff --git a/conf/config.go b/conf/config.go index 4778dfb6..21027dc8 100644 --- a/conf/config.go +++ b/conf/config.go @@ -112,6 +112,9 @@ type GatewayConfig struct { // HTTP is the gateway http endpoint config. HTTP *GatewayHTTPConfig `koanf:"http"` + // Monitoring is the gateway prometheus configuration. + Monitoring *GatewayMonitoringConfig `koanf:"monitoring"` + // WS is the gateway websocket endpoint config. WS *GatewayWSConfig `koanf:"ws"` @@ -122,6 +125,36 @@ type GatewayConfig struct { MethodLimits *MethodLimits `koanf:"method_limits"` } +// GatewayMonitoringConfig is the gateway prometheus configuration. +type GatewayMonitoringConfig struct { + // Host is the host interface on which to start the prometheus http server. Disabled if unset. + Host string `koanf:"host"` + + // Port is the port number on which to start the prometheus http server. + Port int `koanf:"port"` +} + +// Enabled returns true if monitoring is configured. +func (cfg *GatewayMonitoringConfig) Enabled() bool { + if cfg == nil { + return false + } + if cfg.Host == "" { + return false + } + return true +} + +// Address returns the prometheus listen address. +// +// Returns empty string if monitoring is not configured. +func (cfg *GatewayMonitoringConfig) Address() string { + if !cfg.Enabled() { + return "" + } + return fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) +} + // Validate validates the gateway configuration. func (cfg *GatewayConfig) Validate() error { // TODO: diff --git a/conf/server.yml b/conf/server.yml index de7abf0d..aff069a7 100644 --- a/conf/server.yml +++ b/conf/server.yml @@ -32,5 +32,8 @@ gateway: ws: host: "localhost" port: 8546 + monitoring: + host: "" # Disabled. + port: 9999 method_limits: get_logs_max_rounds: 100 diff --git a/conf/tests.yml b/conf/tests.yml index f8d83706..a15a907d 100644 --- a/conf/tests.yml +++ b/conf/tests.yml @@ -29,5 +29,8 @@ gateway: ws: host: "localhost" port: 8546 + monitoring: + host: "localhost" + port: 9999 method_limits: get_logs_max_rounds: 100 diff --git a/docker/emerald-dev/emerald-dev.yml b/docker/emerald-dev/emerald-dev.yml index 09ef6af8..e5621b8c 100644 --- a/docker/emerald-dev/emerald-dev.yml +++ b/docker/emerald-dev/emerald-dev.yml @@ -26,5 +26,8 @@ gateway: host: "0.0.0.0" port: 8546 cors: ["*"] + monitoring: + host: "0.0.0.0" + port: 9999 method_limits: get_logs_max_rounds: 100 diff --git a/rpc/apis.go b/rpc/apis.go index 91f127c0..882b0dfe 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -12,6 +12,7 @@ import ( "github.com/oasisprotocol/emerald-web3-gateway/indexer" "github.com/oasisprotocol/emerald-web3-gateway/rpc/eth" "github.com/oasisprotocol/emerald-web3-gateway/rpc/eth/filters" + ethmetrics "github.com/oasisprotocol/emerald-web3-gateway/rpc/eth/metrics" "github.com/oasisprotocol/emerald-web3-gateway/rpc/net" "github.com/oasisprotocol/emerald-web3-gateway/rpc/txpool" "github.com/oasisprotocol/emerald-web3-gateway/rpc/web3" @@ -27,35 +28,49 @@ func GetRPCAPIs( ) []ethRpc.API { var apis []ethRpc.API + web3Service := web3.NewPublicAPI() + ethService := eth.NewPublicAPI(client, logging.GetLogger("eth_rpc"), config.ChainID, backend, config.MethodLimits) + netService := net.NewPublicAPI(config.ChainID) + txpoolService := txpool.NewPublicAPI() + filtersService := filters.NewPublicAPI(client, logging.GetLogger("eth_filters"), backend, eventSystem) + + if config.Monitoring.Enabled() { + web3Service = web3.NewMetricsWrapper(web3Service) + netService = net.NewMetricsWrapper(netService) + ethService = ethmetrics.NewMetricsWrapper(ethService, logging.GetLogger("eth_rpc_metrics"), backend) + txpoolService = txpool.NewMetricsWrapper(txpoolService) + filtersService = filters.NewMetricsWrapper(filtersService) + } + apis = append(apis, ethRpc.API{ Namespace: "web3", Version: "1.0", - Service: web3.NewPublicAPI(), + Service: web3Service, Public: true, }, ethRpc.API{ Namespace: "net", Version: "1.0", - Service: net.NewPublicAPI(config.ChainID), + Service: netService, Public: true, }, ethRpc.API{ Namespace: "eth", Version: "1.0", - Service: eth.NewPublicAPI(client, logging.GetLogger("eth_rpc"), config.ChainID, backend, config.MethodLimits), + Service: ethService, Public: true, }, ethRpc.API{ Namespace: "txpool", Version: "1.0", - Service: txpool.NewPublicAPI(), + Service: txpoolService, Public: true, }, ethRpc.API{ Namespace: "eth", Version: "1.0", - Service: filters.NewPublicAPI(client, logging.GetLogger("eth_filters"), backend, eventSystem), + Service: filtersService, Public: true, }, ) diff --git a/rpc/eth/api.go b/rpc/eth/api.go index bb7e3d04..da102755 100644 --- a/rpc/eth/api.go +++ b/rpc/eth/api.go @@ -52,8 +52,57 @@ const ( revertErrorPrefix = "reverted: " ) -// PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicAPI struct { +// API is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type API interface { + // GetBlockByNumber returns the block identified by number. + GetBlockByNumber(ctx context.Context, blockNum ethrpc.BlockNumber, fullTx bool) (map[string]interface{}, error) + // GetBlockTransactionCountByNumber returns the number of transactions in the block. + GetBlockTransactionCountByNumber(ctx context.Context, blockNum ethrpc.BlockNumber) (hexutil.Uint, error) + // GetStorageAt returns the storage value at the provided position. + GetStorageAt(ctx context.Context, address common.Address, position hexutil.Big, blockNrOrHash ethrpc.BlockNumberOrHash) (hexutil.Big, error) + // GetBalance returns the provided account's balance up to the provided block number. + GetBalance(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (*hexutil.Big, error) + // ChainId return the EIP-155 chain id for the current network. + ChainId() (*hexutil.Big, error) + // GasPrice returns a suggestion for a gas price for legacy transactions. + GasPrice(ctx context.Context) (*hexutil.Big, error) + // GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. + GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (hexutil.Uint, error) + // GetTransactionCount returns the number of transactions the given address has sent for the given block number. + GetTransactionCount(ctx context.Context, ethAddr common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (*hexutil.Uint64, error) + // GetCode returns the contract code at the given address and block number. + GetCode(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (hexutil.Bytes, error) + // Call executes the given transaction on the state for the given block number. + Call(ctx context.Context, args utils.TransactionArgs, blockNrOrHash ethrpc.BlockNumberOrHash, _ *utils.StateOverride) (hexutil.Bytes, error) + // SendRawTransaction send a raw Ethereum transaction. + SendRawTransaction(ctx context.Context, data hexutil.Bytes) (common.Hash, error) + // EstimateGas returns an estimate of gas usage for the given transaction. + EstimateGas(ctx context.Context, args utils.TransactionArgs, blockNum *ethrpc.BlockNumber) (hexutil.Uint64, error) + // GetBlockByHash returns the block identified by hash. + GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (map[string]interface{}, error) + // GetTransactionByHash returns the transaction identified by hash. + GetTransactionByHash(ctx context.Context, hash common.Hash) (*utils.RPCTransaction, error) + // GetTransactionByBlockHashAndIndex returns the transaction for the given block hash and index. + GetTransactionByBlockHashAndIndex(ctx context.Context, blockHash common.Hash, index hexutil.Uint) (*utils.RPCTransaction, error) + // GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. + GetTransactionByBlockNumberAndIndex(ctx context.Context, blockNum ethrpc.BlockNumber, index hexutil.Uint) (*utils.RPCTransaction, error) + // GetTransactionReceipt returns the transaction receipt by hash. + GetTransactionReceipt(ctx context.Context, txHash common.Hash) (map[string]interface{}, error) + // GetLogs returns the ethereum logs. + GetLogs(ctx context.Context, filter filters.FilterCriteria) ([]*ethtypes.Log, error) + // GetBlockHash returns the block hash by the given number. + GetBlockHash(ctx context.Context, blockNum ethrpc.BlockNumber, _ bool) (common.Hash, error) + // BlockNumber returns the latest block number. + BlockNumber(ctx context.Context) (hexutil.Uint64, error) + // Accounts returns the list of accounts available to this node. + Accounts() ([]common.Address, error) + // Mining returns whether or not this node is currently mining. + Mining() bool + // Hashrate returns the current node's hashrate. + Hashrate() hexutil.Uint64 +} + +type publicAPI struct { client client.RuntimeClient backend indexer.Backend chainID uint32 @@ -68,8 +117,8 @@ func NewPublicAPI( chainID uint32, backend indexer.Backend, methodLimits *conf.MethodLimits, -) *PublicAPI { - return &PublicAPI{ +) API { + return &publicAPI{ client: client, chainID: chainID, Logger: logger, @@ -94,7 +143,7 @@ func handleStorageError(logger *logging.Logger, err error) error { } // roundParamFromBlockNum converts special BlockNumber values to the corresponding special round numbers. -func (api *PublicAPI) roundParamFromBlockNum(ctx context.Context, logger *logging.Logger, blockNum ethrpc.BlockNumber) (uint64, error) { +func (api *publicAPI) roundParamFromBlockNum(ctx context.Context, logger *logging.Logger, blockNum ethrpc.BlockNumber) (uint64, error) { switch blockNum { case ethrpc.PendingBlockNumber: // Oasis does not expose a pending block. Use the latest. @@ -129,8 +178,7 @@ func (api *PublicAPI) roundParamFromBlockNum(ctx context.Context, logger *loggin } } -// GetBlockByNumber returns the block identified by number. -func (api *PublicAPI) GetBlockByNumber(ctx context.Context, blockNum ethrpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { +func (api *publicAPI) GetBlockByNumber(ctx context.Context, blockNum ethrpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { logger := api.Logger.With("method", "eth_getBlockByNumber", "block_number", blockNum, "full_tx", fullTx) logger.Debug("request") @@ -147,8 +195,7 @@ func (api *PublicAPI) GetBlockByNumber(ctx context.Context, blockNum ethrpc.Bloc return utils.ConvertToEthBlock(blk, fullTx), nil } -// GetBlockTransactionCountByNumber returns the number of transactions in the block. -func (api *PublicAPI) GetBlockTransactionCountByNumber(ctx context.Context, blockNum ethrpc.BlockNumber) (hexutil.Uint, error) { +func (api *publicAPI) GetBlockTransactionCountByNumber(ctx context.Context, blockNum ethrpc.BlockNumber) (hexutil.Uint, error) { logger := api.Logger.With("method", "eth_getBlockTransactionCountByNumber", "block_number", blockNum) logger.Debug("request") @@ -164,7 +211,7 @@ func (api *PublicAPI) GetBlockTransactionCountByNumber(ctx context.Context, bloc return hexutil.Uint(n), nil } -func (api *PublicAPI) GetStorageAt(ctx context.Context, address common.Address, position hexutil.Big, blockNrOrHash ethrpc.BlockNumberOrHash) (hexutil.Big, error) { +func (api *publicAPI) GetStorageAt(ctx context.Context, address common.Address, position hexutil.Big, blockNrOrHash ethrpc.BlockNumberOrHash) (hexutil.Big, error) { logger := api.Logger.With("method", "eth_getStorageAt", "address", address, "position", position, "block_or_hash", blockNrOrHash) logger.Debug("request") @@ -189,8 +236,7 @@ func (api *PublicAPI) GetStorageAt(ctx context.Context, address common.Address, return hexutil.Big(resultBI), nil } -// GetBalance returns the provided account's balance up to the provided block number. -func (api *PublicAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (*hexutil.Big, error) { +func (api *publicAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (*hexutil.Big, error) { logger := api.Logger.With("method", "eth_getBalance", "address", address, "block_or_hash", blockNrOrHash) logger.Debug("request") @@ -209,15 +255,13 @@ func (api *PublicAPI) GetBalance(ctx context.Context, address common.Address, bl } // nolint:revive,stylecheck -// ChainId return the EIP-155 chain id for the current network. -func (api *PublicAPI) ChainId() (*hexutil.Big, error) { +func (api *publicAPI) ChainId() (*hexutil.Big, error) { logger := api.Logger.With("method", "eth_chainId") logger.Debug("request") return (*hexutil.Big)(big.NewInt(int64(api.chainID))), nil } -// GasPrice returns a suggestion for a gas price for legacy transactions. -func (api *PublicAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { +func (api *publicAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { logger := api.Logger.With("method", "eth_gasPrice") logger.Debug("request") @@ -231,8 +275,7 @@ func (api *PublicAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { return (*hexutil.Big)(nativeMGP.ToBigInt()), nil } -// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. -func (api *PublicAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (hexutil.Uint, error) { +func (api *publicAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (hexutil.Uint, error) { logger := api.Logger.With("method", "eth_getBlockTransactionCountByHash", "block_hash", blockHash.Hex()) logger.Debug("request") @@ -244,8 +287,7 @@ func (api *PublicAPI) GetBlockTransactionCountByHash(ctx context.Context, blockH return hexutil.Uint(n), nil } -// GetTransactionCount returns the number of transactions the given address has sent for the given block number. -func (api *PublicAPI) GetTransactionCount(ctx context.Context, ethAddr common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (*hexutil.Uint64, error) { +func (api *publicAPI) GetTransactionCount(ctx context.Context, ethAddr common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (*hexutil.Uint64, error) { logger := api.Logger.With("method", "eth_getBlockTransactionCount", "address", ethAddr, "block_or_hash", blockNrOrHash) logger.Debug("request") @@ -265,8 +307,7 @@ func (api *PublicAPI) GetTransactionCount(ctx context.Context, ethAddr common.Ad return (*hexutil.Uint64)(&nonce), nil } -// GetCode returns the contract code at the given address and block number. -func (api *PublicAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (hexutil.Bytes, error) { +func (api *publicAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (hexutil.Bytes, error) { logger := api.Logger.With("method", "eth_getCode", "address", address, "block_or_hash", blockNrOrHash) logger.Debug("request") @@ -295,7 +336,7 @@ func (e *RevertError) ErrorData() interface{} { } // NewRevertError returns an revert error with ABI encoded revert reason. -func (api *PublicAPI) NewRevertError(revertErr error) *RevertError { +func (api *publicAPI) NewRevertError(revertErr error) *RevertError { // ABI encoded function. abiReason := []byte{0x08, 0xc3, 0x79, 0xa0} // Keccak256("Error(string)") @@ -317,9 +358,7 @@ func (api *PublicAPI) NewRevertError(revertErr error) *RevertError { } } -// Call executes the given transaction on the state for the given block number. -// This function doesn't make any changes in the evm state of blockchain. -func (api *PublicAPI) Call(ctx context.Context, args utils.TransactionArgs, blockNrOrHash ethrpc.BlockNumberOrHash, _ *utils.StateOverride) (hexutil.Bytes, error) { +func (api *publicAPI) Call(ctx context.Context, args utils.TransactionArgs, blockNrOrHash ethrpc.BlockNumberOrHash, _ *utils.StateOverride) (hexutil.Bytes, error) { logger := api.Logger.With("method", "eth_call", "block_or_hash", blockNrOrHash) logger.Debug("request", "args", args) var ( @@ -380,8 +419,7 @@ func (api *PublicAPI) Call(ctx context.Context, args utils.TransactionArgs, bloc return res, nil } -// SendRawTransaction send a raw Ethereum transaction. -func (api *PublicAPI) SendRawTransaction(ctx context.Context, data hexutil.Bytes) (common.Hash, error) { +func (api *publicAPI) SendRawTransaction(ctx context.Context, data hexutil.Bytes) (common.Hash, error) { logger := api.Logger.With("method", "eth_sendRawTransaction") logger.Debug("request", "length", len(data)) @@ -409,8 +447,7 @@ func (api *PublicAPI) SendRawTransaction(ctx context.Context, data hexutil.Bytes return ethTx.Hash(), nil } -// EstimateGas returns an estimate of gas usage for the given transaction . -func (api *PublicAPI) EstimateGas(ctx context.Context, args utils.TransactionArgs, blockNum *ethrpc.BlockNumber) (hexutil.Uint64, error) { +func (api *publicAPI) EstimateGas(ctx context.Context, args utils.TransactionArgs, blockNum *ethrpc.BlockNumber) (hexutil.Uint64, error) { logger := api.Logger.With("method", "eth_estimateGas", "block_number", blockNum) logger.Debug("request", "args", args) @@ -458,8 +495,7 @@ func (api *PublicAPI) EstimateGas(ctx context.Context, args utils.TransactionArg return hexutil.Uint64(gas), nil } -// GetBlockByHash returns the block identified by hash. -func (api *PublicAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (map[string]interface{}, error) { +func (api *publicAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (map[string]interface{}, error) { logger := api.Logger.With("method", "eth_getBlockByHash", "block_hash", blockHash, "full_tx", fullTx) logger.Debug("request") @@ -471,8 +507,7 @@ func (api *PublicAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, return utils.ConvertToEthBlock(blk, fullTx), nil } -// GetTransactionByHash returns the transaction identified by hash. -func (api *PublicAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*utils.RPCTransaction, error) { +func (api *publicAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*utils.RPCTransaction, error) { logger := api.Logger.With("method", "eth_getTransactionByHash", "hash", hash) logger.Debug("request") @@ -484,8 +519,7 @@ func (api *PublicAPI) GetTransactionByHash(ctx context.Context, hash common.Hash return utils.NewRPCTransaction(dbTx), nil } -// GetTransactionByBlockHashAndIndex returns the transaction for the given block hash and index. -func (api *PublicAPI) GetTransactionByBlockHashAndIndex(ctx context.Context, blockHash common.Hash, index hexutil.Uint) (*utils.RPCTransaction, error) { +func (api *publicAPI) GetTransactionByBlockHashAndIndex(ctx context.Context, blockHash common.Hash, index hexutil.Uint) (*utils.RPCTransaction, error) { logger := api.Logger.With("method", "eth_getTransactionByBlockHashAndIndex", "block_hash", blockHash, "index", index) logger.Debug("request") @@ -501,8 +535,7 @@ func (api *PublicAPI) GetTransactionByBlockHashAndIndex(ctx context.Context, blo return utils.NewRPCTransaction(dbBlock.Transactions[index]), nil } -// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. -func (api *PublicAPI) GetTransactionByBlockNumberAndIndex(ctx context.Context, blockNum ethrpc.BlockNumber, index hexutil.Uint) (*utils.RPCTransaction, error) { +func (api *publicAPI) GetTransactionByBlockNumberAndIndex(ctx context.Context, blockNum ethrpc.BlockNumber, index hexutil.Uint) (*utils.RPCTransaction, error) { logger := api.Logger.With("method", "eth_getTransactionByNumberAndIndex", "block_number", blockNum, "index", index) logger.Debug("request") @@ -518,8 +551,7 @@ func (api *PublicAPI) GetTransactionByBlockNumberAndIndex(ctx context.Context, b return api.GetTransactionByBlockHashAndIndex(ctx, blockHash, index) } -// GetTransactionReceipt returns the transaction receipt by hash. -func (api *PublicAPI) GetTransactionReceipt(ctx context.Context, txHash common.Hash) (map[string]interface{}, error) { +func (api *publicAPI) GetTransactionReceipt(ctx context.Context, txHash common.Hash) (map[string]interface{}, error) { logger := api.Logger.With("method", "eth_getTransactionReceipt", "hash", txHash) logger.Debug("request") @@ -532,7 +564,7 @@ func (api *PublicAPI) GetTransactionReceipt(ctx context.Context, txHash common.H } // getStartEndRounds is a helper for fetching start and end rounds parameters. -func (api *PublicAPI) getStartEndRounds(ctx context.Context, logger *logging.Logger, filter filters.FilterCriteria) (uint64, uint64, error) { +func (api *publicAPI) getStartEndRounds(ctx context.Context, logger *logging.Logger, filter filters.FilterCriteria) (uint64, uint64, error) { if filter.BlockHash != nil { round, err := api.backend.QueryBlockRound(ctx, *filter.BlockHash) if err != nil { @@ -561,8 +593,7 @@ func (api *PublicAPI) getStartEndRounds(ctx context.Context, logger *logging.Log return start, end, nil } -// GetLogs returns the ethereum logs. -func (api *PublicAPI) GetLogs(ctx context.Context, filter filters.FilterCriteria) ([]*ethtypes.Log, error) { +func (api *publicAPI) GetLogs(ctx context.Context, filter filters.FilterCriteria) ([]*ethtypes.Log, error) { logger := api.Logger.With("method", "eth_getLogs") logger.Debug("request", "filter", filter) @@ -620,8 +651,7 @@ func (api *PublicAPI) GetLogs(ctx context.Context, filter filters.FilterCriteria return filtered, nil } -// GetBlockHash returns the block hash by the given number. -func (api *PublicAPI) GetBlockHash(ctx context.Context, blockNum ethrpc.BlockNumber, _ bool) (common.Hash, error) { +func (api *publicAPI) GetBlockHash(ctx context.Context, blockNum ethrpc.BlockNumber, _ bool) (common.Hash, error) { logger := api.Logger.With("method", "eth_getBlockHash", "block_num", blockNum) logger.Debug("request") @@ -632,8 +662,7 @@ func (api *PublicAPI) GetBlockHash(ctx context.Context, blockNum ethrpc.BlockNum return api.backend.QueryBlockHash(ctx, round) } -// BlockNumber returns the latest block number. -func (api *PublicAPI) BlockNumber(ctx context.Context) (hexutil.Uint64, error) { +func (api *publicAPI) BlockNumber(ctx context.Context) (hexutil.Uint64, error) { logger := api.Logger.With("method", "eth_getBlockNumber") logger.Debug("request") @@ -648,8 +677,7 @@ func (api *PublicAPI) BlockNumber(ctx context.Context) (hexutil.Uint64, error) { return hexutil.Uint64(blockNumber), nil } -// Accounts returns the list of accounts available to this node. -func (api *PublicAPI) Accounts() ([]common.Address, error) { +func (api *publicAPI) Accounts() ([]common.Address, error) { logger := api.Logger.With("method", "eth_getAccounts") logger.Debug("request") @@ -657,22 +685,20 @@ func (api *PublicAPI) Accounts() ([]common.Address, error) { return addresses, nil } -// Mining returns whether or not this node is currently mining. Always false. -func (api *PublicAPI) Mining() bool { +func (api *publicAPI) Mining() bool { logger := api.Logger.With("method", "eth_mining") logger.Debug("request") return false } -// Hashrate returns the current node's hashrate. Always 0. -func (api *PublicAPI) Hashrate() hexutil.Uint64 { +func (api *publicAPI) Hashrate() hexutil.Uint64 { logger := api.Logger.With("method", "eth_hashrate") logger.Debug("request") return 0 } // getBlockRound returns the block round from BlockNumberOrHash. -func (api *PublicAPI) getBlockRound(ctx context.Context, logger *logging.Logger, blockNrOrHash ethrpc.BlockNumberOrHash) (uint64, error) { +func (api *publicAPI) getBlockRound(ctx context.Context, logger *logging.Logger, blockNrOrHash ethrpc.BlockNumberOrHash) (uint64, error) { switch { // case if block number and blockhash is specified are handling by the BlockNumberOrHash type. case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: diff --git a/rpc/eth/filters/api.go b/rpc/eth/filters/api.go index 8163e4e7..ecc03ded 100644 --- a/rpc/eth/filters/api.go +++ b/rpc/eth/filters/api.go @@ -15,8 +15,15 @@ import ( "github.com/oasisprotocol/emerald-web3-gateway/indexer" ) -// PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicFilterAPI struct { +// API is the eth_ prefixed set of APIs in the filtering Web3 JSON-RPC spec. +type API interface { + // NewHeads send a notification each time a new (header) block is appended to the chain. + NewHeads(ctx context.Context) (*ethrpc.Subscription, error) + // Logs creates a subscription that fires for all new log that match the given filter criteria. + Logs(ctx context.Context, crit ethfilters.FilterCriteria) (*ethrpc.Subscription, error) +} + +type publicFilterAPI struct { client client.RuntimeClient backend indexer.Backend Logger *logging.Logger @@ -29,8 +36,8 @@ func NewPublicAPI( logger *logging.Logger, backend indexer.Backend, eventSystem *eventFilters.EventSystem, -) *PublicFilterAPI { - return &PublicFilterAPI{ +) API { + return &publicFilterAPI{ client: client, Logger: logger, backend: backend, @@ -38,7 +45,7 @@ func NewPublicAPI( } } -func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*ethrpc.Subscription, error) { +func (api *publicFilterAPI) NewHeads(ctx context.Context) (*ethrpc.Subscription, error) { notifier, supported := ethrpc.NotifierFromContext(ctx) if !supported { return ðrpc.Subscription{}, ethrpc.ErrNotificationsUnsupported @@ -71,7 +78,7 @@ func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*ethrpc.Subscription, return rpcSub, nil } -func (api *PublicFilterAPI) Logs(ctx context.Context, crit ethfilters.FilterCriteria) (*ethrpc.Subscription, error) { +func (api *publicFilterAPI) Logs(ctx context.Context, crit ethfilters.FilterCriteria) (*ethrpc.Subscription, error) { notifier, supported := ethrpc.NotifierFromContext(ctx) if !supported { return ðrpc.Subscription{}, ethrpc.ErrNotificationsUnsupported diff --git a/rpc/eth/filters/metrics.go b/rpc/eth/filters/metrics.go new file mode 100644 index 00000000..358a39e1 --- /dev/null +++ b/rpc/eth/filters/metrics.go @@ -0,0 +1,90 @@ +package filters + +import ( + "context" + + ethfilters "github.com/ethereum/go-ethereum/eth/filters" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/oasisprotocol/emerald-web3-gateway/rpc/metrics" +) + +var ( + durations = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "oasis_emerald_web3_gateway_subscription_seconds", + // Buckets ranging from 1 second to 24 hours. + Buckets: []float64{1, 10, 30, 60, 600, 1800, 3600, 7200, 21600, 86400}, + Help: "Histogram for the eth subscription API subscriptions duration.", + }, + []string{"method_name"}, + ) + inflightSubs = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "oasis_emerald_web3_gateway_subscription_inflight", + Help: "Number of concurrent eth inflight subscriptions.", + }, + []string{"method_name"}, + ) +) + +type metricsWrapper struct { + api API +} + +// Logs implements API. +func (m *metricsWrapper) Logs(ctx context.Context, crit ethfilters.FilterCriteria) (rpcSub *ethrpc.Subscription, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_logs") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + inflightSubs := inflightSubs.WithLabelValues("eth_logs") + duration := durations.WithLabelValues("eth_logs") + + // Measure subscirpiton duration and concurrent subscriptions. + timer := prometheus.NewTimer(duration) + inflightSubs.Inc() + + rpcSub, err = m.api.Logs(ctx, crit) + go func() { + // Wait for subscription to unsubscribe. + <-rpcSub.Err() + timer.ObserveDuration() + // Decrement in-flight. + inflightSubs.Dec() + }() + + return rpcSub, err +} + +// NewHeads implements API. +func (m *metricsWrapper) NewHeads(ctx context.Context) (rpcSub *ethrpc.Subscription, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_newHeads") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + inflightSubs := inflightSubs.WithLabelValues("eth_newHeads") + duration := durations.WithLabelValues("eth_newHeads") + + // Measure subscirpiton duration and concurrent subscriptions. + timer := prometheus.NewTimer(duration) + inflightSubs.Inc() + + rpcSub, err = m.api.NewHeads(ctx) + go func() { + // Wait for subscription to unsubscribe. + <-rpcSub.Err() + timer.ObserveDuration() + // Decrement in-flight. + inflightSubs.Dec() + }() + + return rpcSub, err +} + +// NewMetricsWrapper returns an instrumanted API service. +func NewMetricsWrapper(api API) API { + return &metricsWrapper{ + api, + } +} diff --git a/rpc/eth/metrics/api.go b/rpc/eth/metrics/api.go new file mode 100644 index 00000000..f3d4ceb8 --- /dev/null +++ b/rpc/eth/metrics/api.go @@ -0,0 +1,322 @@ +package metrics + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/oasisprotocol/oasis-core/go/common/logging" + + "github.com/oasisprotocol/emerald-web3-gateway/indexer" + "github.com/oasisprotocol/emerald-web3-gateway/rpc/eth" + "github.com/oasisprotocol/emerald-web3-gateway/rpc/metrics" + "github.com/oasisprotocol/emerald-web3-gateway/rpc/utils" +) + +var requestHeights = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "oasis_emerald_web3_gateway_api_request_heights", + Buckets: []float64{0, 1, 2, 3, 5, 10, 50, 100, 500, 1000}, + Help: "Histogram of eth API request heights (difference from the latest height).", + }, + []string{"method_name"}, +) + +type metricsWrapper struct { + api eth.API + + logger *logging.Logger + backend indexer.Backend +} + +func (m *metricsWrapper) latestAndRequestHeights(ctx context.Context, blockNum ethrpc.BlockNumber) (uint64, uint64, error) { + var height uint64 + + switch blockNum { + case ethrpc.PendingBlockNumber, ethrpc.LatestBlockNumber: + // Same as latest height by definition. + return 0, 0, nil + case ethrpc.EarliestBlockNumber: + // Fetch earliest block. + var err error + height, err = m.backend.QueryLastRetainedRound(ctx) + if err != nil { + return 0, 0, err + } + // Continue below. + fallthrough + default: + if int64(blockNum) < 0 { + return 0, 0, eth.ErrMalformedBlockNumber + } + if height == 0 { + height = uint64(blockNum) + } + + // Fetch latest block height. + // Note: as this is called async, the latest height could have changed since the request + // was served, but this likely only results in a off-by-one measured height difference + // which is good enough for instrumentation. + latest, err := m.backend.QueryLastIndexedRound(ctx) + if err != nil { + return 0, 0, err + } + return latest, height, nil + } +} + +func (m *metricsWrapper) meassureRequestHeightDiff(method string, blockNum ethrpc.BlockNumber) { + // Don't use the parent (request) context here, so that this method can run + // without blocking the request. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + latest, request, err := m.latestAndRequestHeights(ctx, blockNum) + if err != nil { + m.logger.Debug("fetching latest and request heights error", "err", err) + return + } + if request > latest { + // This is a query for a non-existing block, skip. + return + } + requestHeights.WithLabelValues(method).Observe(float64(latest - request)) +} + +// Accounts implements eth.API. +func (m *metricsWrapper) Accounts() (res []common.Address, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_accounts") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.Accounts() + return +} + +// BlockNumber implements eth.API. +func (m *metricsWrapper) BlockNumber(ctx context.Context) (res hexutil.Uint64, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_blockNumber") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.BlockNumber(ctx) + return +} + +// Call implements eth.API. +func (m *metricsWrapper) Call(ctx context.Context, args utils.TransactionArgs, blockNrOrHash ethrpc.BlockNumberOrHash, so *utils.StateOverride) (res hexutil.Bytes, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_call") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.Call(ctx, args, blockNrOrHash, so) + return +} + +// ChainId implements eth.API. +// nolint:revive,stylecheck +func (m *metricsWrapper) ChainId() (res *hexutil.Big, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_chainId") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.ChainId() + return +} + +// EstimateGas implements eth.API. +func (m *metricsWrapper) EstimateGas(ctx context.Context, args utils.TransactionArgs, blockNum *ethrpc.BlockNumber) (res hexutil.Uint64, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_estimateGas") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.EstimateGas(ctx, args, blockNum) + return +} + +// GasPrice implements eth.API. +func (m *metricsWrapper) GasPrice(ctx context.Context) (res *hexutil.Big, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_gasPrice") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GasPrice(ctx) + return +} + +// GetBalance implements eth.API. +func (m *metricsWrapper) GetBalance(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (res *hexutil.Big, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBalance") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetBalance(ctx, address, blockNrOrHash) + return +} + +// GetBlockByHash implements eth.API. +func (m *metricsWrapper) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (res map[string]interface{}, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBlockByHash") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetBlockByHash(ctx, blockHash, fullTx) + return +} + +// GetBlockByNumber implements eth.API. +func (m *metricsWrapper) GetBlockByNumber(ctx context.Context, blockNum ethrpc.BlockNumber, fullTx bool) (res map[string]interface{}, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBlockByNumber") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetBlockByNumber(ctx, blockNum, fullTx) + + // Measure request height difference from latest height. + go m.meassureRequestHeightDiff("eth_getBlockByNumber", blockNum) + + return +} + +// GetBlockHash implements eth.API. +func (m *metricsWrapper) GetBlockHash(ctx context.Context, blockNum ethrpc.BlockNumber, b bool) (res common.Hash, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBlockHash") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetBlockHash(ctx, blockNum, b) + + // Measure request height difference from latest height. + go m.meassureRequestHeightDiff("eth_getBlockHash", blockNum) + + return +} + +// GetBlockTransactionCountByHash implements eth.API. +func (m *metricsWrapper) GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (res hexutil.Uint, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBlockTransationCountByHash") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetBlockTransactionCountByHash(ctx, blockHash) + return +} + +// GetBlockTransactionCountByNumber implements eth.API. +func (m *metricsWrapper) GetBlockTransactionCountByNumber(ctx context.Context, blockNum ethrpc.BlockNumber) (res hexutil.Uint, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getBlockTransationCountByNumber") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetBlockTransactionCountByNumber(ctx, blockNum) + + // Measure request height difference from latest height. + go m.meassureRequestHeightDiff("eth_getBlockTransationCountByNumber", blockNum) + + return +} + +// GetCode implements eth.API. +func (m *metricsWrapper) GetCode(ctx context.Context, address common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (res hexutil.Bytes, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getCode") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetCode(ctx, address, blockNrOrHash) + return +} + +// GetLogs implements eth.API. +func (m *metricsWrapper) GetLogs(ctx context.Context, filter filters.FilterCriteria) (res []*ethtypes.Log, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getLogs") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetLogs(ctx, filter) + return +} + +// GetStorageAt implements eth.API. +func (m *metricsWrapper) GetStorageAt(ctx context.Context, address common.Address, position hexutil.Big, blockNrOrHash ethrpc.BlockNumberOrHash) (res hexutil.Big, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getStorageAt") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetStorageAt(ctx, address, position, blockNrOrHash) + return +} + +// GetTransactionByBlockHashAndIndex implements eth.API. +func (m *metricsWrapper) GetTransactionByBlockHashAndIndex(ctx context.Context, blockHash common.Hash, index hexutil.Uint) (res *utils.RPCTransaction, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getTransactionByBlockHashAndIndex") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetTransactionByBlockHashAndIndex(ctx, blockHash, index) + return +} + +// GetTransactionByBlockNumberAndIndex implements eth.API. +func (m *metricsWrapper) GetTransactionByBlockNumberAndIndex(ctx context.Context, blockNum ethrpc.BlockNumber, index hexutil.Uint) (res *utils.RPCTransaction, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getTransactionByBlockNumberAndIndex") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetTransactionByBlockNumberAndIndex(ctx, blockNum, index) + + // Measure request height difference from latest height. + go m.meassureRequestHeightDiff("eth_getTransactionByBlockNumberAndIndex", blockNum) + + return +} + +// GetTransactionByHash implements eth.API. +func (m *metricsWrapper) GetTransactionByHash(ctx context.Context, hash common.Hash) (res *utils.RPCTransaction, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getTransactionByHash") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetTransactionByHash(ctx, hash) + return +} + +// GetTransactionCount implements eth.API. +func (m *metricsWrapper) GetTransactionCount(ctx context.Context, ethAddr common.Address, blockNrOrHash ethrpc.BlockNumberOrHash) (h *hexutil.Uint64, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getTransactionCount") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + h, err = m.api.GetTransactionCount(ctx, ethAddr, blockNrOrHash) + return +} + +// GetTransactionReceipt implements eth.API. +func (m *metricsWrapper) GetTransactionReceipt(ctx context.Context, txHash common.Hash) (res map[string]interface{}, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_getTransactionReceipt") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.GetTransactionReceipt(ctx, txHash) + return +} + +// Hashrate implements eth.API. +func (m *metricsWrapper) Hashrate() hexutil.Uint64 { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_hashrate") + defer metrics.InstrumentCaller(r, s, f, i, d, nil)() + + return m.api.Hashrate() +} + +// Mining implements eth.API. +func (m *metricsWrapper) Mining() bool { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_mining") + defer metrics.InstrumentCaller(r, s, f, i, d, nil)() + + return m.api.Mining() +} + +// SendRawTransaction implements eth.API. +func (m *metricsWrapper) SendRawTransaction(ctx context.Context, data hexutil.Bytes) (h common.Hash, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("eth_sendRawTransaction") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + h, err = m.api.SendRawTransaction(ctx, data) + return +} + +// NewMetricsWrapper returns an instrumanted API service. +func NewMetricsWrapper(api eth.API, logger *logging.Logger, backend indexer.Backend) eth.API { + return &metricsWrapper{ + api, + logger, + backend, + } +} diff --git a/rpc/metrics/metrics.go b/rpc/metrics/metrics.go new file mode 100644 index 00000000..5baf286d --- /dev/null +++ b/rpc/metrics/metrics.go @@ -0,0 +1,48 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + durations = promauto.NewHistogramVec(prometheus.HistogramOpts{Name: "oasis_emerald_web3_gateway_api_seconds", Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 20, 30}, Help: "Histogram for the eth API requests duration."}, []string{"method_name"}) + requests = promauto.NewCounterVec(prometheus.CounterOpts{Name: "oasis_emerald_web3_gateway_api_request", Help: "Counter for API requests."}, []string{"method_name"}) + failures = promauto.NewCounterVec(prometheus.CounterOpts{Name: "oasis_emerald_web3_gateway_api_failure", Help: "Counter for API request failures."}, []string{"method_name"}) + successes = promauto.NewCounterVec(prometheus.CounterOpts{Name: "oasis_emerald_web3_gateway_api_success", Help: "Counter for API successful requests."}, []string{"method_name"}) + inflight = promauto.NewGaugeVec(prometheus.GaugeOpts{Name: "oasis_emerald_web3_gateway_api_inflight", Help: "Number of inflight API request."}, []string{"method_name"}) +) + +// GetAPIMethodMetrics returns the method metrics for the specified API call. +func GetAPIMethodMetrics(method string) (prometheus.Counter, prometheus.Counter, prometheus.Counter, prometheus.Gauge, prometheus.Observer) { + return requests.WithLabelValues(method), + successes.WithLabelValues(method), + failures.WithLabelValues(method), + inflight.WithLabelValues(method), + durations.WithLabelValues(method) +} + +// InstrumentCaller instruments the caller method. +// +// Use InstrumentCaller is usually used the following way: +// func InstrumentMe() (err error) { +// r, s, f, i, d := metrics.GetAPIMethodMetrics("method") +// defer metrics.InstrumentCaller(r, s, f, i, d, &err)() +// // Do actual work. +// } +func InstrumentCaller(count prometheus.Counter, success prometheus.Counter, failures prometheus.Counter, inflight prometheus.Gauge, duration prometheus.Observer, err *error) func() { + timer := prometheus.NewTimer(duration) + inflight.Inc() + return func() { + // Duration. + timer.ObserveDuration() + // Counts. + count.Inc() + if err != nil && *err != nil { + failures.Inc() + } else { + success.Inc() + } + inflight.Dec() + } +} diff --git a/rpc/net/api.go b/rpc/net/api.go index eab3bdb0..beb36cc0 100644 --- a/rpc/net/api.go +++ b/rpc/net/api.go @@ -4,17 +4,21 @@ import ( "fmt" ) -// PublicAPI is the net_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicAPI struct { +// API is the net_ prefixed set of APIs in the Web3 JSON-RPC spec. +type API interface { + // Version returns the current ethereum protocol version. + Version() string +} + +type publicAPI struct { chainID uint32 } // NewPublicAPI creates an instance of the Web3 API. -func NewPublicAPI(chainID uint32) *PublicAPI { - return &PublicAPI{chainID} +func NewPublicAPI(chainID uint32) API { + return &publicAPI{chainID} } -// Version returns the current ethereum protocol version. -func (api *PublicAPI) Version() string { +func (api *publicAPI) Version() string { return fmt.Sprintf("%d", api.chainID) } diff --git a/rpc/net/metrics.go b/rpc/net/metrics.go new file mode 100644 index 00000000..31e6aef2 --- /dev/null +++ b/rpc/net/metrics.go @@ -0,0 +1,24 @@ +package net + +import ( + "github.com/oasisprotocol/emerald-web3-gateway/rpc/metrics" +) + +type metricsWrapper struct { + api API +} + +// ClientVersion implements API. +func (m *metricsWrapper) Version() string { + r, s, f, i, d := metrics.GetAPIMethodMetrics("net_version") + defer metrics.InstrumentCaller(r, s, f, i, d, nil)() + + return m.api.Version() +} + +// NewMetricsWrapper returns an instrumanted API service. +func NewMetricsWrapper(api API) API { + return &metricsWrapper{ + api, + } +} diff --git a/rpc/txpool/api.go b/rpc/txpool/api.go index 00f8b2f9..c4679f26 100644 --- a/rpc/txpool/api.go +++ b/rpc/txpool/api.go @@ -1,15 +1,19 @@ package txpool -// PublicAPI is the txpool_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicAPI struct{} +// API is the txpool_ prefixed set of APIs in the Web3 JSON-RPC spec. +type API interface { + // Content returns the (always empty) contents of the txpool. + Content() (map[string][]interface{}, error) +} + +type publicAPI struct{} // NewPublicAPI creates an instance of the Web3 API. -func NewPublicAPI() *PublicAPI { - return &PublicAPI{} +func NewPublicAPI() API { + return &publicAPI{} } -// Content returns the (always empty) contents of the txpool. -func (api *PublicAPI) Content() (map[string][]interface{}, error) { +func (api *publicAPI) Content() (map[string][]interface{}, error) { m := make(map[string][]interface{}) m["pending"] = []interface{}{} return m, nil diff --git a/rpc/txpool/metrics.go b/rpc/txpool/metrics.go new file mode 100644 index 00000000..fae07661 --- /dev/null +++ b/rpc/txpool/metrics.go @@ -0,0 +1,23 @@ +package txpool + +import "github.com/oasisprotocol/emerald-web3-gateway/rpc/metrics" + +type metricsWrapper struct { + api API +} + +// Content implements API. +func (m *metricsWrapper) Content() (res map[string][]interface{}, err error) { + r, s, f, i, d := metrics.GetAPIMethodMetrics("txpool_content") + defer metrics.InstrumentCaller(r, s, f, i, d, &err)() + + res, err = m.api.Content() + return +} + +// NewMetricsWrapper returns an instrumanted API service. +func NewMetricsWrapper(api API) API { + return &metricsWrapper{ + api, + } +} diff --git a/rpc/web3/api.go b/rpc/web3/api.go index 64ac64f1..e8c96541 100644 --- a/rpc/web3/api.go +++ b/rpc/web3/api.go @@ -9,20 +9,25 @@ import ( "github.com/oasisprotocol/emerald-web3-gateway/version" ) -// PublicAPI is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicAPI struct{} +// API is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec. +type API interface { + // Sha3 returns the keccak-256 hash of the passed-in input. + Sha3(input hexutil.Bytes) hexutil.Bytes + // ClientVersion returns the current client info. + ClientVersion() string +} + +type publicAPI struct{} // NewPublicAPI creates an instance of the Web3 API. -func NewPublicAPI() *PublicAPI { - return &PublicAPI{} +func NewPublicAPI() API { + return &publicAPI{} } -// Sha3 returns the keccak-256 hash of the passed-in input. -func (a *PublicAPI) Sha3(input hexutil.Bytes) hexutil.Bytes { +func (a *publicAPI) Sha3(input hexutil.Bytes) hexutil.Bytes { return crypto.Keccak256(input) } -// ClientVersion returns the current client info. -func (a *PublicAPI) ClientVersion() string { +func (a *publicAPI) ClientVersion() string { return fmt.Sprintf("oasis/%s/%s", version.Software, version.Toolchain) } diff --git a/rpc/web3/metrics.go b/rpc/web3/metrics.go new file mode 100644 index 00000000..73bb4e03 --- /dev/null +++ b/rpc/web3/metrics.go @@ -0,0 +1,34 @@ +package web3 + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/oasisprotocol/emerald-web3-gateway/rpc/metrics" +) + +type metricsWrapper struct { + api API +} + +// ClientVersion implements API. +func (m *metricsWrapper) ClientVersion() string { + r, s, f, i, d := metrics.GetAPIMethodMetrics("web3_clientVersion") + defer metrics.InstrumentCaller(r, s, f, i, d, nil)() + + return m.api.ClientVersion() +} + +// Sha3 implements API. +func (m *metricsWrapper) Sha3(input hexutil.Bytes) hexutil.Bytes { + r, s, f, i, d := metrics.GetAPIMethodMetrics("web3_sha3") + defer metrics.InstrumentCaller(r, s, f, i, d, nil)() + + return m.api.Sha3(input) +} + +// NewMetricsWrapper returns an instrumanted API service. +func NewMetricsWrapper(api API) API { + return &metricsWrapper{ + api, + } +} diff --git a/server/server.go b/server/server.go index f78c9c00..976e0926 100644 --- a/server/server.go +++ b/server/server.go @@ -4,10 +4,15 @@ import ( "context" "errors" "fmt" + "net" + "net/http" "sync" + "time" "github.com/ethereum/go-ethereum/rpc" + "github.com/gorilla/mux" "github.com/oasisprotocol/oasis-core/go/common/logging" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/oasisprotocol/emerald-web3-gateway/conf" "github.com/oasisprotocol/emerald-web3-gateway/storage" @@ -114,6 +119,26 @@ func New(ctx context.Context, conf *conf.GatewayConfig) (*Web3Gateway, error) { return server, nil } +func startPrometheusServer(address string) error { + router := mux.NewRouter() + router.Handle("/metrics", promhttp.Handler()) + + server := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + ln, err := net.Listen("tcp", address) + if err != nil { + return err + } + go func() { _ = server.Serve(ln) }() + + return nil +} + // Start Web3Gateway can only be started once. func (srv *Web3Gateway) Start() error { srv.startStopLock.Lock() @@ -139,7 +164,14 @@ func (srv *Web3Gateway) Start() error { srv.doClose(nil) return err } - return err + + if srv.config.Monitoring.Enabled() { + address := srv.config.Monitoring.Address() + srv.logger.Info("starting prometheus metrics server", "address", address) + return startPrometheusServer(address) + } + + return nil } // Close stops the Web3Gateway Server and releases resources acquired in