diff --git a/README.md b/README.md index 129a015..859ce0f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # relayer_exporter -Prometheus exporter for ibc clients. +Prometheus exporter for ibc clients and wallet balance. Returns metrics about clients expiration date. +Returns wallet balance for configured addresses. ## Configuration Exporter needs config file in yaml format like following @@ -16,16 +17,26 @@ github: org: archway-network repo: networks dir: _IBC + +accounts: + - address: archway1l2al7y78500h5akvgt8exwnkpmf2zmk8ky9ht3 + chainId: constantine-3 + denom: aconst ``` During startup it fetches IBC paths from github based on provided config. If env var GITHUB_TOKEN is provided it will be used to make authenticated requests to GitHub API. Using provided RPC endpoints it gets clients expiration dates for fetched paths. +For provided accounts it fetches wallet balances using endpoints defined in rpc list. + ## Metrics ``` # HELP cosmos_ibc_client_expiry Returns light client expiry in unixtime. # TYPE cosmos_ibc_client_expiry gauge cosmos_ibc_client_expiry{client_id="07-tendermint-23",host_chain_id="archway-1",target_chain_id="agoric-3"} 1.695283384e+09 cosmos_ibc_client_expiry{client_id="07-tendermint-75",host_chain_id="agoric-3",target_chain_id="archway-1"} 1.69528327e+09 +# HELP cosmos_wallet_balance Returns wallet balance for an address on a chain +# TYPE cosmos_wallet_balance gauge +cosmos_wallet_balance{account="archway1l2al7y78500h5akvgt8exwnkpmf2zmk8ky9ht3",chain_id="constantine-3",denom="aconst",status="success"} 4.64e+18 ``` diff --git a/cmd/relayer_exporter/relayer_exporter.go b/cmd/relayer_exporter/relayer_exporter.go index f374919..b9b19a7 100644 --- a/cmd/relayer_exporter/relayer_exporter.go +++ b/cmd/relayer_exporter/relayer_exporter.go @@ -57,7 +57,13 @@ func main() { Paths: paths, } + balancesCollector := collector.WalletBalanceCollector{ + RPCs: rpcs, + Accounts: cfg.Accounts, + } + prometheus.MustRegister(clientsCollector) + prometheus.MustRegister(balancesCollector) http.Handle("/metrics", promhttp.Handler()) diff --git a/config.yaml b/config.yaml index 0afbbd4..f475a59 100644 --- a/config.yaml +++ b/config.yaml @@ -33,3 +33,8 @@ github: org: archway-network repo: networks dir: _IBC + +accounts: + - address: archway1l2al7y78500h5akvgt8exwnkpmf2zmk8ky9ht3 + chainId: constantine-3 + denom: aconst diff --git a/go.mod b/go.mod index 3bb3486..520083d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/archway-network/relayer_exporter go 1.21 require ( + cosmossdk.io/math v1.0.1 github.com/caarlos0/env/v9 v9.0.0 github.com/cosmos/relayer/v2 v2.4.1 github.com/google/go-cmp v0.5.9 @@ -24,7 +25,6 @@ require ( cosmossdk.io/depinject v1.0.0-alpha.3 // indirect cosmossdk.io/errors v1.0.0-beta.7 // indirect cosmossdk.io/log v1.1.0 // indirect - cosmossdk.io/math v1.0.1 // indirect cosmossdk.io/tools/rosetta v0.2.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect diff --git a/pkg/account/account.go b/pkg/account/account.go new file mode 100644 index 0000000..726fcf6 --- /dev/null +++ b/pkg/account/account.go @@ -0,0 +1,46 @@ +package account + +import ( + "context" + + "cosmossdk.io/math" + + "github.com/archway-network/relayer_exporter/pkg/chain" +) + +const ( + successStatus = "success" + errorStatus = "error" +) + +type Account struct { + Address string `yaml:"address"` + Denom string `yaml:"denom"` + ChainID string `yaml:"chainId"` + Balance math.Int + Status string +} + +func (a *Account) GetBalance(rpcs map[string]string) error { + chain, err := chain.PrepChain(chain.Info{ + ChainID: a.ChainID, + RPCAddr: rpcs[a.ChainID], + }) + if err != nil { + return err + } + + ctx := context.Background() + + coins, err := chain.ChainProvider.QueryBalanceWithAddress(ctx, a.Address) + if err != nil { + a.Status = errorStatus + + return err + } + + a.Balance = coins.AmountOf(a.Denom) + a.Status = successStatus + + return nil +} diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go new file mode 100644 index 0000000..9f5e00b --- /dev/null +++ b/pkg/chain/chain.go @@ -0,0 +1,48 @@ +package chain + +import ( + "context" + + "github.com/cosmos/relayer/v2/relayer" + "github.com/cosmos/relayer/v2/relayer/chains/cosmos" +) + +const ( + rpcTimeout = "10s" + keyringBackend = "test" +) + +type Info struct { + ChainID string + RPCAddr string + ClientID string +} + +func PrepChain(info Info) (*relayer.Chain, error) { + chain := relayer.Chain{} + providerConfig := cosmos.CosmosProviderConfig{ + ChainID: info.ChainID, + Timeout: rpcTimeout, + KeyringBackend: keyringBackend, + RPCAddr: info.RPCAddr, + } + + provider, err := providerConfig.NewProvider(nil, "", false, info.ChainID) + if err != nil { + return nil, err + } + + err = provider.Init(context.Background()) + if err != nil { + return nil, err + } + + chain.ChainProvider = provider + + err = chain.SetPath(&relayer.PathEnd{ClientID: info.ClientID}) + if err != nil { + return nil, err + } + + return &chain, nil +} diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index d04d117..5c79796 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -1,6 +1,10 @@ package collector import ( + "math/big" + "sync" + + "github.com/archway-network/relayer_exporter/pkg/account" "github.com/archway-network/relayer_exporter/pkg/ibc" log "github.com/archway-network/relayer_exporter/pkg/logger" "github.com/cosmos/relayer/v2/relayer" @@ -9,15 +13,21 @@ import ( ) const ( - clientExpiryMetricName = "cosmos_ibc_client_expiry" - configuredChainMetricName = "cosmos_relayer_configured_chain" - relayerUpMetricName = "cosmos_relayer_up" + clientExpiryMetricName = "cosmos_ibc_client_expiry" + walletBalanceMetricName = "cosmos_wallet_balance" ) -var clientExpiry = prometheus.NewDesc( - clientExpiryMetricName, - "Returns light client expiry in unixtime.", - []string{"host_chain_id", "client_id", "target_chain_id"}, nil, +var ( + clientExpiry = prometheus.NewDesc( + clientExpiryMetricName, + "Returns light client expiry in unixtime.", + []string{"host_chain_id", "client_id", "target_chain_id"}, nil, + ) + walletBalance = prometheus.NewDesc( + walletBalanceMetricName, + "Returns wallet balance for an address on a chain.", + []string{"account", "chain_id", "denom", "status"}, nil, + ) ) type IBCClientsCollector struct { @@ -25,7 +35,14 @@ type IBCClientsCollector struct { Paths []*relayer.IBCdata } -func (cc IBCClientsCollector) Describe(_ chan<- *prometheus.Desc) {} +type WalletBalanceCollector struct { + RPCs map[string]string + Accounts []account.Account +} + +func (cc IBCClientsCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- clientExpiry +} func (cc IBCClientsCollector) Collect(ch chan<- prometheus.Metric) { log.Debug("Start collecting", zap.String("metric", clientExpiryMetricName)) @@ -50,3 +67,42 @@ func (cc IBCClientsCollector) Collect(ch chan<- prometheus.Metric) { log.Debug("Stop collecting", zap.String("metric", clientExpiryMetricName)) } + +func (wb WalletBalanceCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- walletBalance +} + +func (wb WalletBalanceCollector) Collect(ch chan<- prometheus.Metric) { + log.Debug("Start collecting", zap.String("metric", walletBalanceMetricName)) + + var wg sync.WaitGroup + + for _, a := range wb.Accounts { + wg.Add(1) + + go func(account account.Account) { + defer wg.Done() + + balance := 0.0 + + err := account.GetBalance(wb.RPCs) + if err != nil { + log.Error(err.Error(), zap.Any("account", account)) + } else { + // Convert to a big float to get a float64 for metrics + balance, _ = big.NewFloat(0.0).SetInt(account.Balance.BigInt()).Float64() + } + + ch <- prometheus.MustNewConstMetric( + walletBalance, + prometheus.GaugeValue, + balance, + []string{account.Address, account.ChainID, account.Denom, account.Status}..., + ) + }(a) + } + + wg.Wait() + + log.Debug("Stop collecting", zap.String("metric", walletBalanceMetricName)) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index d3820ca..093a5a7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-github/v55/github" "gopkg.in/yaml.v3" + "github.com/archway-network/relayer_exporter/pkg/account" log "github.com/archway-network/relayer_exporter/pkg/logger" ) @@ -22,8 +23,9 @@ type RPC struct { } type Config struct { - RPCs []RPC `yaml:"rpc"` - GitHub struct { + Accounts []account.Account `yaml:"accounts"` + RPCs []RPC `yaml:"rpc"` + GitHub struct { Org string `yaml:"org"` Repo string `yaml:"repo"` IBCDir string `yaml:"dir"` diff --git a/pkg/ibc/ibc.go b/pkg/ibc/ibc.go index 35a12ae..d4ba302 100644 --- a/pkg/ibc/ibc.go +++ b/pkg/ibc/ibc.go @@ -5,23 +5,12 @@ import ( "fmt" "time" + "github.com/archway-network/relayer_exporter/pkg/chain" log "github.com/archway-network/relayer_exporter/pkg/logger" "github.com/cosmos/relayer/v2/relayer" - "github.com/cosmos/relayer/v2/relayer/chains/cosmos" "github.com/google/go-cmp/cmp" ) -const ( - rpcTimeout = "10s" - keyringBackend = "test" -) - -type ChainData struct { - ChainID string - RPCAddr string - ClientID string -} - type ClientsInfo struct { ChainA *relayer.Chain ChainAClientInfo relayer.ClientStateInfo @@ -66,26 +55,26 @@ func GetClientsInfos(ibcs []*relayer.IBCdata, rpcs map[string]string) []ClientsI func GetClientsInfo(ibc *relayer.IBCdata, rpcs map[string]string) (ClientsInfo, error) { clientsInfo := ClientsInfo{} - cdA := ChainData{ + cdA := chain.Info{ ChainID: ibc.Chain1.ChainName, RPCAddr: rpcs[ibc.Chain1.ChainName], ClientID: ibc.Chain1.ClientID, } - chainA, err := prepChain(cdA) + chainA, err := chain.PrepChain(cdA) if err != nil { return ClientsInfo{}, fmt.Errorf("Error: %w for %v", err, cdA) } clientsInfo.ChainA = chainA - cdB := ChainData{ + cdB := chain.Info{ ChainID: ibc.Chain2.ChainName, RPCAddr: rpcs[ibc.Chain2.ChainName], ClientID: ibc.Chain2.ClientID, } - chainB, err := prepChain(cdB) + chainB, err := chain.PrepChain(cdB) if err != nil { return ClientsInfo{}, fmt.Errorf("Error: %w for %v", err, cdB) } @@ -106,32 +95,3 @@ func GetClientsInfo(ibc *relayer.IBCdata, rpcs map[string]string) (ClientsInfo, return clientsInfo, nil } - -func prepChain(cd ChainData) (*relayer.Chain, error) { - chain := relayer.Chain{} - providerConfig := cosmos.CosmosProviderConfig{ - ChainID: cd.ChainID, - Timeout: rpcTimeout, - KeyringBackend: keyringBackend, - RPCAddr: cd.RPCAddr, - } - - provider, err := providerConfig.NewProvider(nil, "", false, cd.ChainID) - if err != nil { - return nil, err - } - - err = provider.Init(context.Background()) - if err != nil { - return nil, err - } - - chain.ChainProvider = provider - - err = chain.SetPath(&relayer.PathEnd{ClientID: cd.ClientID}) - if err != nil { - return nil, err - } - - return &chain, nil -}