From 96f98374a46500870fe10ef340965050574fc689 Mon Sep 17 00:00:00 2001 From: Shahbaz Nazir Date: Tue, 12 Dec 2023 11:27:32 +0100 Subject: [PATCH] improvements: clean code (#32) * refactor and log improvements * add test cases * use existing logger instead of adding new one * consolidate contexts * add config validations --- .gitignore | 6 + cmd/relayer_exporter/relayer_exporter.go | 15 +- config.yaml | 78 +++++--- go.mod | 5 + go.sum | 19 +- pkg/chain/chain.go | 10 +- pkg/collector/collector.go | 217 +--------------------- pkg/collector/ibc_collector.go | 167 +++++++++++++++++ pkg/collector/wallet_balance_collector.go | 69 +++++++ pkg/config/config.go | 175 ++++++++++++----- pkg/config/config_test.go | 118 ++++++++---- pkg/ibc/ibc.go | 36 ++-- pkg/logger/logger.go | 4 + 13 files changed, 555 insertions(+), 364 deletions(-) create mode 100644 pkg/collector/ibc_collector.go create mode 100644 pkg/collector/wallet_balance_collector.go diff --git a/.gitignore b/.gitignore index b7142e8..149d9a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,12 @@ *.so *.dylib +# vscode +.vscode + +# direnv +.envrc + # Test binary, built with `go test -c` *.test diff --git a/cmd/relayer_exporter/relayer_exporter.go b/cmd/relayer_exporter/relayer_exporter.go index 4176d73..cc67f48 100644 --- a/cmd/relayer_exporter/relayer_exporter.go +++ b/cmd/relayer_exporter/relayer_exporter.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "fmt" "net/http" @@ -8,6 +9,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" "github.com/archway-network/relayer_exporter/pkg/collector" "github.com/archway-network/relayer_exporter/pkg/config" @@ -45,20 +47,25 @@ func main() { log.Info( fmt.Sprintf( - "Getting IBC paths from %s/%s/%s on GitHub", + "Github IBC registry: %s/%s", cfg.GitHub.Org, cfg.GitHub.Repo, - cfg.GitHub.IBCDir, ), + zap.String("Mainnet Directory", cfg.GitHub.IBCDir), + zap.String("Testnet Directory", cfg.GitHub.TestnetsIBCDir), ) + ctx := context.Background() // TODO: Add a feature to refresh paths at configured interval - paths, err := cfg.IBCPaths() + paths, err := cfg.IBCPaths(ctx) if err != nil { log.Fatal(err.Error()) } - rpcs := cfg.GetRPCsMap() + rpcs, err := cfg.GetRPCsMap(paths) + if err != nil { + log.Fatal(err.Error()) + } ibcCollector := collector.IBCCollector{ RPCs: rpcs, diff --git a/config.yaml b/config.yaml index 5eb3574..f9b2e14 100644 --- a/config.yaml +++ b/config.yaml @@ -1,26 +1,28 @@ ---- +github: + org: archway-network + repo: networks + dir: _IBC + testnetsDir: testnets/_IBC rpc: + # mainnets - chainName: archway chainId: archway-1 url: https://rpc.mainnet.archway.io:443 - chainName: agoric chainId: agoric-3 - url: https://main.rpc.agoric.net:443 + url: https://agoric.rpc.kjnodes.com:443 - chainName: axelar chainId: axelar-dojo-1 - url: https://axelar-rpc.polkachu.com:443 - - chainName: axelartestnet - chainId: axelar-testnet-lisbon-3 - url: https://rpc-axelar-testnet.imperator.co:443 - - chainName: archwaytestnet - chainId: constantine-3 - url: https://rpc.constantine.archway.tech:443 + url: https://rpc-1.axelar.nodes.guru:443 - chainName: bitcanna chainId: bitcanna-1 url: https://rpc.bitcanna.io:443 - chainName: cosmoshub chainId: cosmoshub-4 url: https://cosmoshub-rpc.stakely.io:443 + - chainName: decentr + chainId: mainnet-3 + url: https://poseidon.mainnet.decentr.xyz:443 - chainName: jackal chainId: jackal-1 url: https://jackal-rpc.polkachu.com:443 @@ -29,45 +31,61 @@ rpc: url: https://juno-rpc.publicnode.com:443 - chainName: kujira chainId: kaiyo-1 - url: https://kujira-rpc.polkachu.com:443 + url: https://kujira-rpc.publicnode.com:443 - chainName: noble chainId: noble-1 url: https://noble-rpc.polkachu.com:443 - chainName: nois chainId: nois-1 url: https://nois.rpc.kjnodes.com:443 - - chainName: osmosistestnet - chainId: osmo-test-5 - url: https://rpc.osmotest5.osmosis.zone:443 + - chainName: omniflixhub + chainId: omniflixhub-1 + url: https://omniflix.kingnodes.com:443 - chainName: osmosis chainId: osmosis-1 url: https://osmosis-rpc.stakely.io:443 - chainName: quicksilver chainId: quicksilver-2 url: https://rpc.quicksilver.zone:443 - - chainName: akashtestnet - chainId: sandbox-01 - url: https://rpc.sandbox-01.aksh.pw:443 - chainName: umee chainId: umee-1 url: https://rpc-umee.mzonder.com:443 - chainName: gravitybridge chainId: gravity-bridge-3 url: https://gravitychain.io:26657 - - chainName: omniflixhub - chainId: omniflixhub-1 - url: https://rpc-omniflix.mzonder.com:443 - - chainName: decentr - chainId: mainnet-3 - url: https://poseidon.mainnet.decentr.xyz:443 + - chainName: secretnetwork + chainId: secret-4 + url: https://rpc.secret.express:443 + - chainName: terra2 + chainId: phoenix-1 + url: https://terra-rpc.stakely.io:443 + - chainName: comdex + chainId: comdex-1 + url: https://rpc.comdex.one:443 + - chainName: neutron + chainId: neutron-1 + url: https://rpc-kralum.neutron-1.neutron.org:443 + - chainName: stargaze + chainId: stargaze-1 + url: https://rpc.stargaze-apis.com:443 -github: - org: archway-network - repo: networks - dir: _IBC - testnetsDir: testnets/_IBC + # testnets + - chainName: archwaytestnet + chainId: constantine-3 + url: https://rpc.constantine.archway.tech:443 + - chainName: axelartestnet + chainId: axelar-testnet-lisbon-3 + url: https://axelar-testnet-rpc.qubelabs.io:443 + - chainName: osmosistestnet + chainId: osmo-test-5 + url: https://rpc.osmotest5.osmosis.zone:443 accounts: - - address: archway1l2al7y78500h5akvgt8exwnkpmf2zmk8ky9ht3 - chainName: archwaytestnet - denom: aconst + # Foundation + - address: archway1gpyqzc0aerc85cpk2cm8ec6zkc95x5yqrakskv + chainName: archway + denom: aarch + # PhiLabs + - address: archway1ktka5q3cnsy3ar7qwj2huzz6qj9q4ys7h74l9y + chainName: archway + denom: aarch diff --git a/go.mod b/go.mod index 5b329fa..1a921a7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/caarlos0/env/v9 v9.0.0 github.com/cosmos/ibc-go/v7 v7.2.0 github.com/cosmos/relayer/v2 v2.4.1 + github.com/go-playground/validator/v10 v10.16.0 github.com/google/go-github/v55 v55.0.0 github.com/prometheus/client_golang v1.15.0 github.com/stretchr/testify v1.8.4 @@ -73,9 +74,12 @@ require ( github.com/ethereum/go-ethereum v1.10.26 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -116,6 +120,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/klauspost/compress v1.16.3 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/lib/pq v1.10.7 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/linxGnu/grocksdb v1.7.16 // indirect diff --git a/go.sum b/go.sum index af2fa97..10251ef 100644 --- a/go.sum +++ b/go.sum @@ -463,6 +463,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -486,13 +488,17 @@ github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -772,8 +778,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= @@ -1026,6 +1032,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index 224179f..3dcbd9a 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -3,9 +3,9 @@ package chain import ( "context" + log "github.com/archway-network/relayer_exporter/pkg/logger" "github.com/cosmos/relayer/v2/relayer" "github.com/cosmos/relayer/v2/relayer/chains/cosmos" - "go.uber.org/zap" ) const ( @@ -20,9 +20,7 @@ type Info struct { Timeout string } -func PrepChain(info Info) (*relayer.Chain, error) { - logger := zap.NewNop() - +func PrepChain(ctx context.Context, info Info) (*relayer.Chain, error) { timeout := rpcTimeout if info.Timeout != "" { timeout = info.Timeout @@ -40,12 +38,12 @@ func PrepChain(info Info) (*relayer.Chain, error) { return nil, err } - err = provider.Init(context.Background()) + err = provider.Init(ctx) if err != nil { return nil, err } - chain := relayer.NewChain(logger, provider, false) + chain := relayer.NewChain(log.GetLogger(), provider, false) err = chain.SetPath(&relayer.PathEnd{ClientID: info.ClientID}) if err != nil { diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index 5cd6ddd..09d58a7 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -1,228 +1,19 @@ package collector import ( - "fmt" - "math/big" - "reflect" + "context" "regexp" "strings" - "sync" - - "github.com/prometheus/client_golang/prometheus" - "go.uber.org/zap" "github.com/archway-network/relayer_exporter/pkg/config" - "github.com/archway-network/relayer_exporter/pkg/ibc" - log "github.com/archway-network/relayer_exporter/pkg/logger" ) const ( - successStatus = "success" - errorStatus = "error" - clientExpiryMetricName = "cosmos_ibc_client_expiry" - walletBalanceMetricName = "cosmos_wallet_balance" - channelStuckPacketsMetricName = "cosmos_ibc_stuck_packets" -) - -var ( - clientExpiry = prometheus.NewDesc( - clientExpiryMetricName, - "Returns light client expiry in unixtime.", - []string{ - "host_chain_id", - "client_id", - "target_chain_id", - "discord_ids", - "status", - }, - nil, - ) - channelStuckPackets = prometheus.NewDesc( - channelStuckPacketsMetricName, - "Returns stuck packets for a channel.", - []string{ - "src_channel_id", - "dst_channel_id", - "src_chain_id", - "dst_chain_id", - "src_chain_name", - "dst_chain_name", - "discord_ids", - "status", - }, - nil, - ) - walletBalance = prometheus.NewDesc( - walletBalanceMetricName, - "Returns wallet balance for an address on a chain.", - []string{"account", "chain_id", "denom", "status"}, nil, - ) + successStatus = "success" + errorStatus = "error" ) -type IBCCollector struct { - RPCs *map[string]config.RPC - Paths []*config.IBCData -} - -type WalletBalanceCollector struct { - RPCs *map[string]config.RPC - Accounts []config.Account -} - -func (cc IBCCollector) Describe(ch chan<- *prometheus.Desc) { - ch <- clientExpiry - ch <- channelStuckPackets -} - -func (cc IBCCollector) Collect(ch chan<- prometheus.Metric) { - log.Debug( - "Start collecting", - zap.String( - "metrics", - fmt.Sprintf("%s, %s", clientExpiryMetricName, channelStuckPacketsMetricName), - ), - ) - - var wg sync.WaitGroup - - for _, p := range cc.Paths { - wg.Add(1) - - go func(path *config.IBCData) { - defer wg.Done() - - discordIDs := getDiscordIDs(path.Operators) - - // Client info - ci, err := ibc.GetClientsInfo(path, cc.RPCs) - status := successStatus - - if err != nil { - status = errorStatus - - log.Error(err.Error()) - } - - ch <- prometheus.MustNewConstMetric( - clientExpiry, - prometheus.GaugeValue, - float64(ci.ChainAClientExpiration.Unix()), - []string{ - (*cc.RPCs)[path.Chain1.ChainName].ChainID, - path.Chain1.ClientID, - (*cc.RPCs)[path.Chain2.ChainName].ChainID, - discordIDs, - status, - }..., - ) - - ch <- prometheus.MustNewConstMetric( - clientExpiry, - prometheus.GaugeValue, - float64(ci.ChainBClientExpiration.Unix()), - []string{ - (*cc.RPCs)[path.Chain2.ChainName].ChainID, - path.Chain2.ClientID, - (*cc.RPCs)[path.Chain1.ChainName].ChainID, - discordIDs, - status, - }..., - ) - - // Stuck packets - status = successStatus - - stuckPackets, err := ibc.GetChannelsInfo(path, cc.RPCs) - if err != nil { - status = errorStatus - - log.Error(err.Error()) - } - - if !reflect.DeepEqual(stuckPackets, ibc.ChannelsInfo{}) { - for _, sp := range stuckPackets.Channels { - ch <- prometheus.MustNewConstMetric( - channelStuckPackets, - prometheus.GaugeValue, - float64(sp.StuckPackets.Source), - []string{ - sp.Source, - sp.Destination, - (*cc.RPCs)[path.Chain1.ChainName].ChainID, - (*cc.RPCs)[path.Chain2.ChainName].ChainID, - path.Chain1.ChainName, - path.Chain2.ChainName, - discordIDs, - status, - }..., - ) - - ch <- prometheus.MustNewConstMetric( - channelStuckPackets, - prometheus.GaugeValue, - float64(sp.StuckPackets.Destination), - []string{ - sp.Destination, - sp.Source, - (*cc.RPCs)[path.Chain2.ChainName].ChainID, - (*cc.RPCs)[path.Chain1.ChainName].ChainID, - path.Chain2.ChainName, - path.Chain1.ChainName, - discordIDs, - status, - }..., - ) - } - } - }(p) - } - - wg.Wait() - - 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 config.Account) { - defer wg.Done() - - balance := 0.0 - status := successStatus - - err := account.GetBalance(wb.RPCs) - if err != nil { - status = errorStatus - - 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, (*wb.RPCs)[account.ChainName].ChainID, account.Denom, status}..., - ) - }(a) - } - - wg.Wait() - - log.Debug("Stop collecting", zap.String("metric", walletBalanceMetricName)) -} +var ctx = context.Background() func getDiscordIDs(ops []config.Operator) string { var ids []string diff --git a/pkg/collector/ibc_collector.go b/pkg/collector/ibc_collector.go new file mode 100644 index 0000000..efabaa4 --- /dev/null +++ b/pkg/collector/ibc_collector.go @@ -0,0 +1,167 @@ +package collector + +import ( + "fmt" + "reflect" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" + + "github.com/archway-network/relayer_exporter/pkg/config" + "github.com/archway-network/relayer_exporter/pkg/ibc" + log "github.com/archway-network/relayer_exporter/pkg/logger" +) + +const ( + clientExpiryMetricName = "cosmos_ibc_client_expiry" + channelStuckPacketsMetricName = "cosmos_ibc_stuck_packets" +) + +var ( + clientExpiry = prometheus.NewDesc( + clientExpiryMetricName, + "Returns light client expiry in unixtime.", + []string{ + "host_chain_id", + "client_id", + "target_chain_id", + "discord_ids", + "status", + }, + nil, + ) + channelStuckPackets = prometheus.NewDesc( + channelStuckPacketsMetricName, + "Returns stuck packets for a channel.", + []string{ + "src_channel_id", + "dst_channel_id", + "src_chain_id", + "dst_chain_id", + "src_chain_name", + "dst_chain_name", + "discord_ids", + "status", + }, + nil, + ) +) + +type IBCCollector struct { + RPCs *map[string]config.RPC + Paths []*config.IBCData +} + +func (cc IBCCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- clientExpiry + ch <- channelStuckPackets +} + +func (cc IBCCollector) Collect(ch chan<- prometheus.Metric) { + log.Debug( + "Start collecting", + zap.String( + "metrics", + fmt.Sprintf("%s, %s", clientExpiryMetricName, channelStuckPacketsMetricName), + ), + ) + + var wg sync.WaitGroup + + for _, p := range cc.Paths { + wg.Add(1) + + go func(path *config.IBCData) { + defer wg.Done() + + discordIDs := getDiscordIDs(path.Operators) + + // Client info + ci, err := ibc.GetClientsInfo(ctx, path, cc.RPCs) + status := successStatus + + if err != nil { + status = errorStatus + + log.Error(err.Error()) + } + + ch <- prometheus.MustNewConstMetric( + clientExpiry, + prometheus.GaugeValue, + float64(ci.ChainAClientExpiration.Unix()), + []string{ + (*cc.RPCs)[path.Chain1.ChainName].ChainID, + path.Chain1.ClientID, + (*cc.RPCs)[path.Chain2.ChainName].ChainID, + discordIDs, + status, + }..., + ) + + ch <- prometheus.MustNewConstMetric( + clientExpiry, + prometheus.GaugeValue, + float64(ci.ChainBClientExpiration.Unix()), + []string{ + (*cc.RPCs)[path.Chain2.ChainName].ChainID, + path.Chain2.ClientID, + (*cc.RPCs)[path.Chain1.ChainName].ChainID, + discordIDs, + status, + }..., + ) + + // Stuck packets + status = successStatus + + stuckPackets, err := ibc.GetChannelsInfo(ctx, path, cc.RPCs) + if err != nil { + status = errorStatus + + log.Error(err.Error()) + } + + if !reflect.DeepEqual(stuckPackets, ibc.ChannelsInfo{}) { + for _, sp := range stuckPackets.Channels { + ch <- prometheus.MustNewConstMetric( + channelStuckPackets, + prometheus.GaugeValue, + float64(sp.StuckPackets.Source), + []string{ + sp.Source, + sp.Destination, + (*cc.RPCs)[path.Chain1.ChainName].ChainID, + (*cc.RPCs)[path.Chain2.ChainName].ChainID, + path.Chain1.ChainName, + path.Chain2.ChainName, + discordIDs, + status, + }..., + ) + + ch <- prometheus.MustNewConstMetric( + channelStuckPackets, + prometheus.GaugeValue, + float64(sp.StuckPackets.Destination), + []string{ + sp.Destination, + sp.Source, + (*cc.RPCs)[path.Chain2.ChainName].ChainID, + (*cc.RPCs)[path.Chain1.ChainName].ChainID, + path.Chain2.ChainName, + path.Chain1.ChainName, + discordIDs, + status, + }..., + ) + } + } + }(p) + } + + wg.Wait() + + log.Debug("Stop collecting", zap.String("metric", clientExpiryMetricName)) +} diff --git a/pkg/collector/wallet_balance_collector.go b/pkg/collector/wallet_balance_collector.go new file mode 100644 index 0000000..d91fb09 --- /dev/null +++ b/pkg/collector/wallet_balance_collector.go @@ -0,0 +1,69 @@ +package collector + +import ( + "math/big" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" + + "github.com/archway-network/relayer_exporter/pkg/config" + log "github.com/archway-network/relayer_exporter/pkg/logger" +) + +const ( + walletBalanceMetricName = "cosmos_wallet_balance" +) + +var walletBalance = prometheus.NewDesc( + walletBalanceMetricName, + "Returns wallet balance for an address on a chain.", + []string{"account", "chain_id", "denom", "status"}, nil, +) + +type WalletBalanceCollector struct { + RPCs *map[string]config.RPC + Accounts []config.Account +} + +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 config.Account) { + defer wg.Done() + + balance := 0.0 + status := successStatus + + err := account.GetBalance(ctx, wb.RPCs) + if err != nil { + status = errorStatus + + 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, (*wb.RPCs)[account.ChainName].ChainID, account.Denom, 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 34b2dbe..18f7238 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,11 +4,15 @@ import ( "context" "encoding/json" "errors" + "fmt" + "net/url" "os" + "strconv" "strings" "cosmossdk.io/math" "github.com/caarlos0/env/v9" + "github.com/go-playground/validator/v10" "github.com/google/go-github/v55/github" "gopkg.in/yaml.v3" @@ -18,19 +22,22 @@ import ( const ibcPathSuffix = ".json" -var ErrGitHubClient = errors.New("GitHub client not provided") +var ( + ErrGitHubClient = errors.New("GitHub client not provided") + ErrMissingRPCConfigMsg = "missing RPC config for chain: %s" +) type Account struct { - Address string `yaml:"address"` - Denom string `yaml:"denom"` - ChainName string `yaml:"chainName"` + Address string `yaml:"address" validate:"required"` + Denom string `yaml:"denom" validate:"required"` + ChainName string `yaml:"chainName" validate:"required"` Balance math.Int } type RPC struct { - ChainName string `yaml:"chainName"` - ChainID string `yaml:"chainId"` - URL string `yaml:"url"` + ChainName string `yaml:"chainName" validate:"required"` + ChainID string `yaml:"chainId" validate:"required"` + URL string `yaml:"url" validate:"required,http_url,has_port"` Timeout string `yaml:"timeout"` } @@ -39,45 +46,37 @@ type Config struct { GlobalRPCTimeout string `env:"GLOBAL_RPC_TIMEOUT" envDefault:"5s"` RPCs []RPC `yaml:"rpc"` GitHub struct { - Org string `yaml:"org"` - Repo string `yaml:"repo"` - IBCDir string `yaml:"dir"` + Org string `yaml:"org" validate:"required"` + Repo string `yaml:"repo" validate:"required"` + IBCDir string `yaml:"dir" validate:"required"` TestnetsIBCDir string `yaml:"testnetsDir"` Token string `env:"GITHUB_TOKEN"` - } `yaml:"github"` + } `yaml:"github" validate:"required"` } -type IBCData struct { - Schema string `json:"$schema"` +type IBCChainMeta struct { + ChainName string `json:"chain_name"` + ClientID string `json:"client_id"` + ConnectionID string `json:"connection_id"` +} + +type Channel struct { Chain1 struct { - ChainName string `json:"chain_name"` - ClientID string `json:"client_id"` - ConnectionID string `json:"connection_id"` + ChannelID string `json:"channel_id"` + PortID string `json:"port_id"` } `json:"chain_1"` Chain2 struct { - ChainName string `json:"chain_name"` - ClientID string `json:"client_id"` - ConnectionID string `json:"connection_id"` + ChannelID string `json:"channel_id"` + PortID string `json:"port_id"` } `json:"chain_2"` - Channels []struct { - Chain1 struct { - ChannelID string `json:"channel_id"` - PortID string `json:"port_id"` - } `json:"chain_1"` - Chain2 struct { - ChannelID string `json:"channel_id"` - PortID string `json:"port_id"` - } `json:"chain_2"` - Ordering string `json:"ordering"` - Version string `json:"version"` - Tags struct { - Status string `json:"status"` - Preferred bool `json:"preferred"` - Dex string `json:"dex"` - Properties string `json:"properties"` - } `json:"tags,omitempty"` - } `json:"channels"` - Operators []Operator `json:"operators"` + Ordering string `json:"ordering"` + Version string `json:"version"` + Tags struct { + Status string `json:"status"` + Preferred bool `json:"preferred"` + Dex string `json:"dex"` + Properties string `json:"properties"` + } `json:"tags,omitempty"` } type Operator struct { @@ -92,13 +91,21 @@ type Operator struct { Discord Discord `json:"discord"` } +type IBCData struct { + Schema string `json:"$schema"` + Chain1 IBCChainMeta `json:"chain_1"` + Chain2 IBCChainMeta `json:"chain_2"` + Channels []Channel `json:"channels"` + Operators []Operator `json:"operators"` +} + type Discord struct { Handle string `json:"handle"` ID string `json:"id"` } -func (a *Account) GetBalance(rpcs *map[string]RPC) error { - chain, err := chain.PrepChain(chain.Info{ +func (a *Account) GetBalance(ctx context.Context, rpcs *map[string]RPC) error { + chain, err := chain.PrepChain(ctx, chain.Info{ ChainID: (*rpcs)[a.ChainName].ChainID, RPCAddr: (*rpcs)[a.ChainName].URL, Timeout: (*rpcs)[a.ChainName].Timeout, @@ -107,8 +114,6 @@ func (a *Account) GetBalance(rpcs *map[string]RPC) error { return err } - ctx := context.Background() - coins, err := chain.ChainProvider.QueryBalanceWithAddress(ctx, a.Address) if err != nil { return err @@ -119,7 +124,11 @@ func (a *Account) GetBalance(rpcs *map[string]RPC) error { return nil } -func (c *Config) GetRPCsMap() *map[string]RPC { +// GetRPCsMap uses the provided config file to return a map of chain +// chain_names to RPCs. It uses IBCData already extracted from +// github IBC registry to validate config for missing RPCs and raises +// an error if any are missing. +func (c *Config) GetRPCsMap(ibcPaths []*IBCData) (*map[string]RPC, error) { rpcs := map[string]RPC{} for _, rpc := range c.RPCs { @@ -130,10 +139,23 @@ func (c *Config) GetRPCsMap() *map[string]RPC { rpcs[rpc.ChainName] = rpc } - return &rpcs + // Validate RPCs exist for each IBC path + for _, ibcPath := range ibcPaths { + // Check RPC for chain 1 + if _, ok := rpcs[ibcPath.Chain1.ChainName]; !ok { + return &rpcs, fmt.Errorf(ErrMissingRPCConfigMsg, ibcPath.Chain1.ChainName) + } + + // Check RPC for chain 2 + if _, ok := rpcs[ibcPath.Chain2.ChainName]; !ok { + return &rpcs, fmt.Errorf(ErrMissingRPCConfigMsg, ibcPath.Chain2.ChainName) + } + } + + return &rpcs, nil } -func (c *Config) IBCPaths() ([]*IBCData, error) { +func (c *Config) IBCPaths(ctx context.Context) ([]*IBCData, error) { client := github.NewClient(nil) if c.GitHub.Token != "" { @@ -142,14 +164,14 @@ func (c *Config) IBCPaths() ([]*IBCData, error) { client = github.NewClient(nil).WithAuthToken(c.GitHub.Token) } - paths, err := c.getPaths(c.GitHub.IBCDir, client) + paths, err := c.getPaths(ctx, c.GitHub.IBCDir, client) if err != nil { return nil, err } testnetsPaths := []*IBCData{} if c.GitHub.TestnetsIBCDir != "" { - testnetsPaths, err = c.getPaths(c.GitHub.TestnetsIBCDir, client) + testnetsPaths, err = c.getPaths(ctx, c.GitHub.TestnetsIBCDir, client) if err != nil { return nil, err } @@ -160,13 +182,11 @@ func (c *Config) IBCPaths() ([]*IBCData, error) { return paths, nil } -func (c *Config) getPaths(dir string, client *github.Client) ([]*IBCData, error) { +func (c *Config) getPaths(ctx context.Context, dir string, client *github.Client) ([]*IBCData, error) { if client == nil { return nil, ErrGitHubClient } - ctx := context.Background() - _, ibcDir, _, err := client.Repositories.GetContents(ctx, c.GitHub.Org, c.GitHub.Repo, dir, nil) if err != nil { return nil, err @@ -176,6 +196,7 @@ func (c *Config) getPaths(dir string, client *github.Client) ([]*IBCData, error) for _, file := range ibcDir { if strings.HasSuffix(*file.Path, ibcPathSuffix) { + log.Debug(fmt.Sprintf("Fetching IBC data for %s/%s/%s", c.GitHub.Org, c.GitHub.Repo, *file.Path)) content, _, _, err := client.Repositories.GetContents( ctx, c.GitHub.Org, @@ -205,6 +226,55 @@ func (c *Config) getPaths(dir string, client *github.Client) ([]*IBCData, error) return ibcs, nil } +func (c *Config) Validate() error { + validate := validator.New(validator.WithRequiredStructEnabled()) + + // register custom validation for http url as expected by go relayer i.e. + // http_url must have port defined. + // https://github.com/cosmos/relayer/blob/259b1278264180a2aefc2085f1b55753849c4815/cregistry/chain_info.go#L115 + err := validate.RegisterValidation("has_port", func(fl validator.FieldLevel) bool { + val := fl.Field().String() + urlParsed, err := url.Parse(val) + if err != nil { + return false + } + + port := urlParsed.Port() + + // Port must be a iny <= 65535. + if portNum, err := strconv.ParseInt( + port, 10, 32, + ); err != nil || portNum > 65535 || portNum < 1 { + return false + } + return true + }) + if err != nil { + return err + } + + // validate top level fields + if err := validate.Struct(c); err != nil { + return err + } + + // validate RPCs + for _, rpc := range c.RPCs { + if err := validate.Struct(rpc); err != nil { + return fmt.Errorf("%v for RPC config: %+v", err, rpc) + } + } + + // validate accounts + for _, account := range c.Accounts { + if err := validate.Struct(account); err != nil { + return fmt.Errorf("%v for accounts config: %+v", err, account) + } + } + + return nil +} + func NewConfig(configPath string) (*Config, error) { config := &Config{} @@ -223,5 +293,10 @@ func NewConfig(configPath string) (*Config, error) { return nil, err } + err = config.Validate() + if err != nil { + return nil, err + } + return config, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 61e1588..2932546 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,58 +1,112 @@ package config import ( + "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" ) -func TestGetRPCsMap(t *testing.T) { - rpcs := []RPC{ +func TestGetRPC(t *testing.T) { + testCases := []struct { + name string + rpcs []RPC + paths []*IBCData + resp map[string]RPC + err error + }{ { - ChainName: "archway", - ChainID: "archway-1", - URL: "https://rpc.mainnet.archway.io:443", + name: "No Missing or Invalid RPCs", + rpcs: []RPC{ + { + ChainName: "archway", + ChainID: "archway-1", + URL: "https://rpc.mainnet.archway.io:443", + }, + { + ChainName: "archwaytestnet", + ChainID: "constantine-3", + URL: "https://rpc.constantine.archway.tech:443", + Timeout: "2s", + }, + }, + paths: []*IBCData{}, + resp: map[string]RPC{ + "archway": { + ChainName: "archway", + ChainID: "archway-1", + URL: "https://rpc.mainnet.archway.io:443", + Timeout: "5s", + }, + "archwaytestnet": { + ChainName: "archwaytestnet", + ChainID: "constantine-3", + URL: "https://rpc.constantine.archway.tech:443", + Timeout: "2s", + }, + }, + err: nil, }, { - ChainName: "archwaytestnet", - ChainID: "constantine-3", - URL: "https://rpc.constantine.archway.tech:443", - Timeout: "2s", + name: "Missing RPCs", + rpcs: []RPC{ + { + ChainName: "archway", + ChainID: "archway-1", + URL: "https://rpc.mainnet.archway.io:443", + }, + }, + paths: []*IBCData{ + { + Schema: "$schema", + Chain1: IBCChainMeta{ + ChainName: "archway", + ClientID: "Client1", + ConnectionID: "Connection1", + }, + Chain2: IBCChainMeta{ + ChainName: "archwaytestnet", + ClientID: "Client2", + ConnectionID: "Connection2", + }, + Channels: []Channel{}, + Operators: []Operator{}, + }, + }, + resp: map[string]RPC{ + "archway": { + ChainName: "archway", + ChainID: "archway-1", + URL: "https://rpc.mainnet.archway.io:443", + Timeout: "5s", + }, + }, + err: fmt.Errorf(ErrMissingRPCConfigMsg, "archwaytestnet"), }, } - cfg := Config{ - GlobalRPCTimeout: "5s", - RPCs: rpcs, + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cfg := Config{ + GlobalRPCTimeout: "5s", + RPCs: tc.rpcs, + } + res, err := cfg.GetRPCsMap(tc.paths) + assert.Equal(t, err, tc.err) + assert.Equal(t, &tc.resp, res) + }) } - - exp := map[string]RPC{ - "archway": { - ChainName: "archway", - ChainID: "archway-1", - URL: "https://rpc.mainnet.archway.io:443", - Timeout: "5s", - }, - "archwaytestnet": { - ChainName: "archwaytestnet", - ChainID: "constantine-3", - URL: "https://rpc.constantine.archway.tech:443", - Timeout: "2s", - }, - } - - res := cfg.GetRPCsMap() - - assert.Equal(t, &exp, res) } func TestGetPaths(t *testing.T) { cfg := Config{} + ctx := context.Background() expError := ErrGitHubClient - _, err := cfg.getPaths("_IBC", nil) + _, err := cfg.getPaths(ctx, "_IBC", nil) if err == nil { t.Fatalf("Expected error %q, got no error", expError) } diff --git a/pkg/ibc/ibc.go b/pkg/ibc/ibc.go index 2c26c1b..d0541c4 100644 --- a/pkg/ibc/ibc.go +++ b/pkg/ibc/ibc.go @@ -39,7 +39,7 @@ type Channel struct { } } -func GetClientsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (ClientsInfo, error) { +func GetClientsInfo(ctx context.Context, ibc *config.IBCData, rpcs *map[string]config.RPC) (ClientsInfo, error) { clientsInfo := ClientsInfo{} cdA := chain.Info{ @@ -49,9 +49,9 @@ func GetClientsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (ClientsIn ClientID: ibc.Chain1.ClientID, } - chainA, err := chain.PrepChain(cdA) + chainA, err := chain.PrepChain(ctx, cdA) if err != nil { - return ClientsInfo{}, fmt.Errorf("Error: %w for %v", err, cdA) + return ClientsInfo{}, fmt.Errorf("%w for %v", err, cdA) } clientsInfo.ChainA = chainA @@ -63,22 +63,20 @@ func GetClientsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (ClientsIn ClientID: ibc.Chain2.ClientID, } - chainB, err := chain.PrepChain(cdB) + chainB, err := chain.PrepChain(ctx, cdB) if err != nil { - return ClientsInfo{}, fmt.Errorf("Error: %w for %v", err, cdB) + return ClientsInfo{}, fmt.Errorf("%w for %v", err, cdB) } clientsInfo.ChainB = chainB - ctx := context.Background() - clientsInfo.ChainAClientExpiration, clientsInfo.ChainAClientInfo, err = relayer.QueryClientExpiration( ctx, chainA, chainB, ) if err != nil { - return ClientsInfo{}, fmt.Errorf("Error: %w path %v <-> %v", err, cdA, cdB) + return ClientsInfo{}, fmt.Errorf("%w path %v <-> %v", err, cdA, cdB) } clientsInfo.ChainBClientExpiration, clientsInfo.ChainBClientInfo, err = relayer.QueryClientExpiration( @@ -87,14 +85,13 @@ func GetClientsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (ClientsIn chainA, ) if err != nil { - return ClientsInfo{}, fmt.Errorf("Error: %w path %v <-> %v", err, cdB, cdA) + return ClientsInfo{}, fmt.Errorf("%w path %v <-> %v", err, cdB, cdA) } return clientsInfo, nil } -func GetChannelsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (ChannelsInfo, error) { - ctx := context.Background() +func GetChannelsInfo(ctx context.Context, ibc *config.IBCData, rpcs *map[string]config.RPC) (ChannelsInfo, error) { channelInfo := ChannelsInfo{} // Init channel data @@ -108,13 +105,6 @@ func GetChannelsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (Channels channelInfo.Channels = append(channelInfo.Channels, channel) } - if (*rpcs)[ibc.Chain1.ChainName].ChainID == "" || (*rpcs)[ibc.Chain2.ChainName].ChainID == "" { - return channelInfo, fmt.Errorf( - "Error: RPC data is missing, cannot retrieve channel data: %v", - ibc.Channels, - ) - } - cdA := chain.Info{ ChainID: (*rpcs)[ibc.Chain1.ChainName].ChainID, RPCAddr: (*rpcs)[ibc.Chain1.ChainName].URL, @@ -122,9 +112,9 @@ func GetChannelsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (Channels ClientID: ibc.Chain1.ClientID, } - chainA, err := chain.PrepChain(cdA) + chainA, err := chain.PrepChain(ctx, cdA) if err != nil { - return ChannelsInfo{}, fmt.Errorf("Error: %w for %v", err, cdA) + return ChannelsInfo{}, fmt.Errorf("error: %w for %+v", err, cdA) } cdB := chain.Info{ @@ -134,16 +124,16 @@ func GetChannelsInfo(ibc *config.IBCData, rpcs *map[string]config.RPC) (Channels ClientID: ibc.Chain2.ClientID, } - chainB, err := chain.PrepChain(cdB) + chainB, err := chain.PrepChain(ctx, cdB) if err != nil { - return ChannelsInfo{}, fmt.Errorf("Error: %w for %v", err, cdB) + return ChannelsInfo{}, fmt.Errorf("error: %w for %+v", err, cdB) } // test that RPC endpoints are working if _, _, err := relayer.QueryLatestHeights( ctx, chainA, chainB, ); err != nil { - return channelInfo, fmt.Errorf("Error: %w for %v", err, cdA) + return channelInfo, fmt.Errorf("error: %w for %v", err, cdA) } for i, c := range channelInfo.Channels { diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 3e808d3..61ae8b8 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -30,6 +30,10 @@ func init() { logger.Level() } +func GetLogger() *zap.Logger { + return logger +} + func Info(message string, fields ...zap.Field) { logger.Info(message, fields...) }