From bb6c2f8d05e131294f4b06e590cec1e2a5c8f59f Mon Sep 17 00:00:00 2001 From: Sergey Date: Wed, 21 Feb 2024 03:02:26 +0300 Subject: [PATCH 01/12] feat: add subscriptions --- pkg/alias_manager/alias_manager.go | 12 +-- pkg/app.go | 50 ++++++------ pkg/config/config.go | 39 ++++----- pkg/config/toml_config/chain.go | 90 ++++++++------------- pkg/config/toml_config/reporter.go | 10 +++ pkg/config/toml_config/subscription.go | 104 ++++++++++++++++++++++++ pkg/config/toml_config/toml_config.go | 15 ++++ pkg/config/types/chain.go | 20 +++-- pkg/config/types/reporter.go | 2 + pkg/config/types/subscription.go | 16 ++++ pkg/converter/converter.go | 6 +- pkg/filterer/filterer.go | 108 +++++++++++++++++-------- pkg/reporters/reporter.go | 12 +++ pkg/types/report.go | 7 +- templates/telegram/Tx.html | 2 - 15 files changed, 331 insertions(+), 162 deletions(-) create mode 100644 pkg/config/toml_config/subscription.go create mode 100644 pkg/config/types/subscription.go diff --git a/pkg/alias_manager/alias_manager.go b/pkg/alias_manager/alias_manager.go index e164607..1c4f241 100644 --- a/pkg/alias_manager/alias_manager.go +++ b/pkg/alias_manager/alias_manager.go @@ -4,7 +4,7 @@ import ( "os" "main/pkg/config" - "main/pkg/config/types" + configTypes "main/pkg/config/types" "github.com/BurntSushi/toml" "github.com/rs/zerolog" @@ -14,7 +14,7 @@ type Aliases *map[string]string type TomlAliases map[string]Aliases type ChainAliases struct { - Chain *types.Chain + Chain *configTypes.Chain Aliases Aliases } type AllChainAliases map[string]*ChainAliases @@ -22,7 +22,7 @@ type AllChainAliases map[string]*ChainAliases type AliasManager struct { Logger zerolog.Logger Path string - Chains config.Chains + Chains configTypes.Chains Aliases AllChainAliases } @@ -36,15 +36,15 @@ func (a AllChainAliases) ToTomlAliases() TomlAliases { } type ChainAliasesLinks struct { - Chain *types.Chain - Links map[string]types.Link + Chain *configTypes.Chain + Links map[string]configTypes.Link } func (a AllChainAliases) ToAliasesLinks() []ChainAliasesLinks { aliasesLinks := make([]ChainAliasesLinks, 0) for _, chainAliases := range a { - links := make(map[string]types.Link) + links := make(map[string]configTypes.Link) if chainAliases.Aliases == nil { continue diff --git a/pkg/app.go b/pkg/app.go index 3618edf..8837133 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -10,7 +10,7 @@ import ( "main/pkg/alias_manager" "main/pkg/config" "main/pkg/data_fetcher" - "main/pkg/filterer" + filtererPkg "main/pkg/filterer" loggerPkg "main/pkg/logger" metricsPkg "main/pkg/metrics" nodesManagerPkg "main/pkg/nodes_manager" @@ -23,9 +23,9 @@ type App struct { Logger zerolog.Logger Chains []*configTypes.Chain NodesManager *nodesManagerPkg.NodesManager - Reporters []reportersPkg.Reporter + Reporters reportersPkg.Reporters DataFetchers map[string]*data_fetcher.DataFetcher - Filterers map[string]*filterer.Filterer + Filterer *filtererPkg.Filterer MetricsManager *metricsPkg.Manager Version string @@ -57,10 +57,7 @@ func NewApp(config *config.AppConfig, version string) *App { dataFetchers[chain.Name] = data_fetcher.NewDataFetcher(logger, chain, aliasManager, metricsManager) } - filterers := make(map[string]*filterer.Filterer, len(config.Chains)) - for _, chain := range config.Chains { - filterers[chain.Name] = filterer.NewFilterer(logger, chain, metricsManager) - } + filterer := filtererPkg.NewFilterer(logger, config, metricsManager) return &App{ Logger: logger.With().Str("component", "app").Logger(), @@ -68,7 +65,7 @@ func NewApp(config *config.AppConfig, version string) *App { Reporters: reporters, NodesManager: nodesManager, DataFetchers: dataFetchers, - Filterers: filterers, + Filterer: filterer, MetricsManager: metricsManager, Version: version, } @@ -98,36 +95,39 @@ func (a *App) Start() { for { select { case rawReport := <-a.NodesManager.Channel: - chainFilterer, _ := a.Filterers[rawReport.Chain.Name] fetcher, _ := a.DataFetchers[rawReport.Chain.Name] - reportableFiltered := chainFilterer.Filter(rawReport.Reportable) - if reportableFiltered == nil { + reportablesForReporters := a.Filterer.GetReportableForReporters(rawReport.Reportable) + + if len(reportablesForReporters) == 0 { a.Logger.Debug(). Str("node", rawReport.Node). Str("chain", rawReport.Chain.Name). Str("hash", rawReport.Reportable.GetHash()). - Msg("Got report") + Msg("Got report which is nowhere to send") continue } - report := types.Report{ - Node: rawReport.Node, - Chain: rawReport.Chain, - Reportable: reportableFiltered, - } + for reporterName, reportable := range reportablesForReporters { + report := types.Report{ + Node: rawReport.Node, + Chain: rawReport.Chain, + Reportable: reportable, + } - a.Logger.Info(). - Str("node", report.Node). - Str("chain", report.Chain.Name). - Str("hash", report.Reportable.GetHash()). - Msg("Got report") + a.Logger.Info(). + Str("node", report.Node). + Str("chain", report.Chain.Name). + Str("reporter", reporterName). + Str("hash", report.Reportable.GetHash()). + Msg("Got report") + + a.MetricsManager.LogReport(report) - a.MetricsManager.LogReport(report) + rawReport.Reportable.GetAdditionalData(fetcher) - rawReport.Reportable.GetAdditionalData(fetcher) + reporter := a.Reporters.FindByName(reporterName) - for _, reporter := range a.Reporters { if err := reporter.Send(report); err != nil { a.Logger.Error(). Err(err). diff --git a/pkg/config/config.go b/pkg/config/config.go index 0254378..bc3bfef 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,28 +16,15 @@ import ( "github.com/rs/zerolog" ) -type Chains []*types.Chain - -func (c Chains) FindByName(name string) *types.Chain { - for _, chain := range c { - if chain.Name == name { - return chain - } - } - - return nil -} - -type Reporters []*types.Reporter - type AppConfig struct { - Path string - AliasesPath string - LogConfig LogConfig - Chains Chains - Reporters Reporters - Metrics MetricsConfig - Timezone *time.Location + Path string + AliasesPath string + LogConfig LogConfig + Chains types.Chains + Subscriptions types.Subscriptions + Reporters types.Reporters + Metrics MetricsConfig + Timezone *time.Location } type LogConfig struct { @@ -94,6 +81,9 @@ func FromTomlConfig(c *tomlConfig.TomlConfig, path string) *AppConfig { Reporters: utils.Map(c.Reporters, func(r *tomlConfig.Reporter) *types.Reporter { return r.ToAppConfigReporter() }), + Subscriptions: utils.Map(c.Subscriptions, func(s *tomlConfig.Subscription) *types.Subscription { + return s.ToAppConfigSubscription() + }), Timezone: timezone, } } @@ -109,9 +99,10 @@ func (c *AppConfig) ToTomlConfig() *tomlConfig.TomlConfig { ListenAddr: c.Metrics.ListenAddr, Enabled: null.BoolFrom(c.Metrics.Enabled), }, - Chains: utils.Map(c.Chains, tomlConfig.FromAppConfigChain), - Reporters: utils.Map(c.Reporters, tomlConfig.FromAppConfigReporter), - Timezone: c.Timezone.String(), + Chains: utils.Map(c.Chains, tomlConfig.FromAppConfigChain), + Reporters: utils.Map(c.Reporters, tomlConfig.FromAppConfigReporter), + Subscriptions: utils.Map(c.Subscriptions, tomlConfig.FromAppConfigSubscription), + Timezone: c.Timezone.String(), } } diff --git a/pkg/config/toml_config/chain.go b/pkg/config/toml_config/chain.go index 76bc13e..12cb57a 100644 --- a/pkg/config/toml_config/chain.go +++ b/pkg/config/toml_config/chain.go @@ -5,26 +5,19 @@ import ( "main/pkg/config/types" "github.com/cometbft/cometbft/libs/pubsub/query" - "gopkg.in/guregu/null.v4" ) type Chain struct { - Name string `toml:"name"` - PrettyName string `toml:"pretty-name"` - TendermintNodes []string `toml:"tendermint-nodes"` - APINodes []string `toml:"api-nodes"` - Queries []string `toml:"queries"` - Filters []string `toml:"filters"` - MintscanPrefix string `toml:"mintscan-prefix"` - PingPrefix string `toml:"ping-prefix"` - PingBaseUrl string `default:"https://ping.pub" toml:"ping-base-url"` - Explorer *Explorer `toml:"explorer"` - LogUnknownMessages null.Bool `default:"false" toml:"log-unknown-messages"` - LogUnparsedMessages null.Bool `default:"true" toml:"log-unparsed-messages"` - LogFailedTransactions null.Bool `default:"true" toml:"log-failed-transactions"` - LogNodeErrors null.Bool `default:"true" toml:"log-node-errors"` - FilterInternalMessages null.Bool `default:"true" toml:"filter-internal-messages"` - Denoms DenomInfos `toml:"denoms"` + Name string `toml:"name"` + PrettyName string `toml:"pretty-name"` + TendermintNodes []string `toml:"tendermint-nodes"` + APINodes []string `toml:"api-nodes"` + Queries []string `toml:"queries"` + MintscanPrefix string `toml:"mintscan-prefix"` + PingPrefix string `toml:"ping-prefix"` + PingBaseUrl string `default:"https://ping.pub" toml:"ping-base-url"` + Explorer *Explorer `toml:"explorer"` + Denoms DenomInfos `toml:"denoms"` } func (c *Chain) Validate() error { @@ -50,12 +43,6 @@ func (c *Chain) Validate() error { } } - for index, filter := range c.Filters { - if _, err := query.New(filter); err != nil { - return fmt.Errorf("error in filter %d: %s", index, err) - } - } - for index, denom := range c.Denoms { if err := denom.Validate(); err != nil { return fmt.Errorf("error in denom %d: %s", index, err) @@ -81,46 +68,30 @@ func (c *Chain) ToAppConfigChain() *types.Chain { explorer = c.Explorer.ToAppConfigExplorer() } - filters := make([]query.Query, len(c.Filters)) - for index, filter := range c.Filters { - filters[index] = *query.MustParse(filter) - } - queries := make([]query.Query, len(c.Queries)) for index, q := range c.Queries { queries[index] = *query.MustParse(q) } return &types.Chain{ - Name: c.Name, - PrettyName: c.PrettyName, - TendermintNodes: c.TendermintNodes, - APINodes: c.APINodes, - Queries: queries, - Filters: filters, - Explorer: explorer, - SupportedExplorer: supportedExplorer, - LogUnknownMessages: c.LogUnknownMessages.Bool, - LogUnparsedMessages: c.LogUnparsedMessages.Bool, - LogFailedTransactions: c.LogFailedTransactions.Bool, - LogNodeErrors: c.LogNodeErrors.Bool, - FilterInternalMessages: c.FilterInternalMessages.Bool, - Denoms: c.Denoms.ToAppConfigDenomInfos(), + Name: c.Name, + PrettyName: c.PrettyName, + TendermintNodes: c.TendermintNodes, + APINodes: c.APINodes, + Queries: queries, + Explorer: explorer, + SupportedExplorer: supportedExplorer, + Denoms: c.Denoms.ToAppConfigDenomInfos(), } } func FromAppConfigChain(c *types.Chain) *Chain { chain := &Chain{ - Name: c.Name, - PrettyName: c.PrettyName, - TendermintNodes: c.TendermintNodes, - APINodes: c.APINodes, - LogUnknownMessages: null.BoolFrom(c.LogUnknownMessages), - LogUnparsedMessages: null.BoolFrom(c.LogUnparsedMessages), - LogFailedTransactions: null.BoolFrom(c.LogFailedTransactions), - LogNodeErrors: null.BoolFrom(c.LogNodeErrors), - FilterInternalMessages: null.BoolFrom(c.FilterInternalMessages), - Denoms: TomlConfigDenomsFrom(c.Denoms), + Name: c.Name, + PrettyName: c.PrettyName, + TendermintNodes: c.TendermintNodes, + APINodes: c.APINodes, + Denoms: TomlConfigDenomsFrom(c.Denoms), } if c.SupportedExplorer == nil && c.Explorer != nil { @@ -138,11 +109,6 @@ func FromAppConfigChain(c *types.Chain) *Chain { chain.PingBaseUrl = ping.BaseUrl } - chain.Filters = make([]string, len(c.Filters)) - for index, filter := range c.Filters { - chain.Filters[index] = filter.String() - } - chain.Queries = make([]string, len(c.Queries)) for index, q := range c.Queries { chain.Queries[index] = q.String() @@ -173,3 +139,13 @@ func (chains Chains) Validate() error { return nil } + +func (chains Chains) HasChainByName(name string) bool { + for _, chain := range chains { + if chain.Name == name { + return true + } + } + + return false +} diff --git a/pkg/config/toml_config/reporter.go b/pkg/config/toml_config/reporter.go index 80b5592..7079528 100644 --- a/pkg/config/toml_config/reporter.go +++ b/pkg/config/toml_config/reporter.go @@ -66,6 +66,16 @@ func (reporters Reporters) Validate() error { return nil } +func (reporters Reporters) HasReporterByName(name string) bool { + for _, reporter := range reporters { + if reporter.Name == name { + return true + } + } + + return false +} + func FromAppConfigReporter(reporter *types.Reporter) *Reporter { var telegramConfig *TelegramConfig diff --git a/pkg/config/toml_config/subscription.go b/pkg/config/toml_config/subscription.go new file mode 100644 index 0000000..286c9d0 --- /dev/null +++ b/pkg/config/toml_config/subscription.go @@ -0,0 +1,104 @@ +package toml_config + +import ( + "fmt" + "github.com/cometbft/cometbft/libs/pubsub/query" + "gopkg.in/guregu/null.v4" + "main/pkg/config/types" +) + +type Subscriptions []*Subscription + +type Subscription struct { + Name string `toml:"name"` + Reporter string `toml:"reporter"` + Chain string `toml:"chain"` + Filters []string `toml:"filters"` + LogUnknownMessages null.Bool `default:"false" toml:"log-unknown-messages"` + LogUnparsedMessages null.Bool `default:"true" toml:"log-unparsed-messages"` + LogFailedTransactions null.Bool `default:"true" toml:"log-failed-transactions"` + LogNodeErrors null.Bool `default:"true" toml:"log-node-errors"` + FilterInternalMessages null.Bool `default:"true" toml:"filter-internal-messages"` +} + +func (subscriptions Subscriptions) Validate() error { + for index, subscription := range subscriptions { + if err := subscription.Validate(); err != nil { + return fmt.Errorf("error in subscription %d: %s", index, err) + } + } + + // checking names uniqueness + names := map[string]bool{} + + for _, subscription := range subscriptions { + if _, ok := names[subscription.Name]; ok { + return fmt.Errorf("duplicate subscription name: %s", subscription.Name) + } + + names[subscription.Name] = true + } + + return nil +} + +func (s *Subscription) Validate() error { + if s.Name == "" { + return fmt.Errorf("empty subscription name") + } + + if s.Reporter == "" { + return fmt.Errorf("empty reporter name") + } + + if s.Chain == "" { + return fmt.Errorf("empty chain name") + } + + for index, filter := range s.Filters { + if _, err := query.New(filter); err != nil { + return fmt.Errorf("error in filter %d: %s", index, err) + } + } + + return nil +} + +func (s *Subscription) ToAppConfigSubscription() *types.Subscription { + filters := make([]query.Query, len(s.Filters)) + for index, filter := range s.Filters { + filters[index] = *query.MustParse(filter) + } + + return &types.Subscription{ + Name: s.Name, + Reporter: s.Reporter, + Chain: s.Chain, + Filters: filters, + LogUnknownMessages: s.LogUnknownMessages.Bool, + LogUnparsedMessages: s.LogUnparsedMessages.Bool, + LogFailedTransactions: s.LogFailedTransactions.Bool, + LogNodeErrors: s.LogNodeErrors.Bool, + FilterInternalMessages: s.FilterInternalMessages.Bool, + } +} + +func FromAppConfigSubscription(s *types.Subscription) *Subscription { + subscription := &Subscription{ + Name: s.Name, + Reporter: s.Reporter, + Chain: s.Chain, + LogUnknownMessages: null.BoolFrom(s.LogUnknownMessages), + LogUnparsedMessages: null.BoolFrom(s.LogUnparsedMessages), + LogFailedTransactions: null.BoolFrom(s.LogFailedTransactions), + LogNodeErrors: null.BoolFrom(s.LogNodeErrors), + FilterInternalMessages: null.BoolFrom(s.FilterInternalMessages), + } + + subscription.Filters = make([]string, len(s.Filters)) + for index, filter := range s.Filters { + subscription.Filters[index] = filter.String() + } + + return subscription +} diff --git a/pkg/config/toml_config/toml_config.go b/pkg/config/toml_config/toml_config.go index 7dfb80a..d935789 100644 --- a/pkg/config/toml_config/toml_config.go +++ b/pkg/config/toml_config/toml_config.go @@ -12,6 +12,7 @@ type TomlConfig struct { LogConfig LogConfig `toml:"log"` MetricsConfig MetricsConfig `toml:"metrics"` Chains Chains `toml:"chains"` + Subscriptions Subscriptions `toml:"subscriptions"` Timezone string `default:"Etc/GMT" toml:"timezone"` Reporters Reporters `toml:"reporters"` @@ -39,5 +40,19 @@ func (c *TomlConfig) Validate() error { return fmt.Errorf("error in reporters: %s", err) } + if err := c.Subscriptions.Validate(); err != nil { + return fmt.Errorf("error in subscriptions: %s", err) + } + + for index, subscription := range c.Subscriptions { + if !c.Chains.HasChainByName(subscription.Chain) { + return fmt.Errorf("error in subscription %d: no such chain '%s'", index, subscription.Chain) + } + + if !c.Reporters.HasReporterByName(subscription.Reporter) { + return fmt.Errorf("error in subscription %d: no such chain '%s'", index, subscription.Chain) + } + } + return nil } diff --git a/pkg/config/types/chain.go b/pkg/config/types/chain.go index a277e7e..c3d0a33 100644 --- a/pkg/config/types/chain.go +++ b/pkg/config/types/chain.go @@ -8,6 +8,18 @@ import ( "github.com/rs/zerolog" ) +type Chains []*Chain + +func (c Chains) FindByName(name string) *Chain { + for _, chain := range c { + if chain.Name == name { + return chain + } + } + + return nil +} + type Chain struct { Name string PrettyName string @@ -17,14 +29,6 @@ type Chain struct { Explorer *Explorer SupportedExplorer SupportedExplorer Denoms DenomInfos - - LogUnknownMessages bool - LogUnparsedMessages bool - LogFailedTransactions bool - LogNodeErrors bool - FilterInternalMessages bool - - Filters Filters } func (c Chain) GetName() string { diff --git a/pkg/config/types/reporter.go b/pkg/config/types/reporter.go index f930388..2aaf6fd 100644 --- a/pkg/config/types/reporter.go +++ b/pkg/config/types/reporter.go @@ -1,5 +1,7 @@ package types +type Reporters []*Reporter + type TelegramConfig struct { Chat int64 Token string diff --git a/pkg/config/types/subscription.go b/pkg/config/types/subscription.go new file mode 100644 index 0000000..65e8b73 --- /dev/null +++ b/pkg/config/types/subscription.go @@ -0,0 +1,16 @@ +package types + +type Subscriptions []*Subscription + +type Subscription struct { + Name string + Chain string + Reporter string + Filters Filters + + LogUnknownMessages bool + LogUnparsedMessages bool + LogFailedTransactions bool + LogNodeErrors bool + FilterInternalMessages bool +} diff --git a/pkg/converter/converter.go b/pkg/converter/converter.go index d5e2cdc..91fb27d 100644 --- a/pkg/converter/converter.go +++ b/pkg/converter/converter.go @@ -84,9 +84,9 @@ func (c *Converter) ParseEvent(event jsonRpcTypes.RPCResponse, nodeURL string) t return nil } - c.Logger.Trace(). - Str("values", fmt.Sprintf("%+v", resultEvent.Events)). - Msg("Event values") + //c.Logger.Trace(). + // Str("values", fmt.Sprintf("%+v", resultEvent.Events)). + // Msg("Event values") eventDataTx, ok := resultEvent.Data.(tendermintTypes.EventDataTx) if !ok { diff --git a/pkg/filterer/filterer.go b/pkg/filterer/filterer.go index cf8bd66..a4786d3 100644 --- a/pkg/filterer/filterer.go +++ b/pkg/filterer/filterer.go @@ -2,6 +2,7 @@ package filterer import ( "fmt" + configPkg "main/pkg/config" configTypes "main/pkg/config/types" messagesPkg "main/pkg/messages" metricsPkg "main/pkg/metrics" @@ -12,64 +13,98 @@ import ( ) type Filterer struct { - Logger zerolog.Logger - MetricsManager *metricsPkg.Manager - Chain *configTypes.Chain - lastBlockHeight int64 + Logger zerolog.Logger + MetricsManager *metricsPkg.Manager + Config *configPkg.AppConfig + lastBlockHeights map[string]int64 } func NewFilterer( logger *zerolog.Logger, - chain *configTypes.Chain, + config *configPkg.AppConfig, metricsManager *metricsPkg.Manager, ) *Filterer { return &Filterer{ Logger: logger.With(). Str("component", "filterer"). - Str("chain", chain.Name). Logger(), - MetricsManager: metricsManager, - Chain: chain, - lastBlockHeight: 0, + MetricsManager: metricsManager, + Config: config, + lastBlockHeights: map[string]int64{}, + } +} + +func (f *Filterer) GetReportableForReporters( + reportable types.Reportable, +) map[string]types.Reportable { + reportables := make(map[string]types.Reportable) + + for _, subscription := range f.Config.Subscriptions { + chain := f.Config.Chains.FindByName(subscription.Chain) + + reportableFiltered := f.FilterForChainAndSubscription( + reportable, + chain, + subscription, + ) + + if reportableFiltered != nil { + f.Logger.Info(). + Str("type", reportable.Type()). + Str("subscription_name", subscription.Name). + Msg("Got report for subscription") + reportables[subscription.Reporter] = reportableFiltered + } else { + f.Logger.Info(). + Str("type", reportable.Type()). + Str("subscription_name", subscription.Name). + Msg("No report for subscription") + } } + + return reportables } -func (f *Filterer) Filter(reportable types.Reportable) types.Reportable { +func (f *Filterer) FilterForChainAndSubscription( + reportable types.Reportable, + chain *configTypes.Chain, + subscription *configTypes.Subscription, +) types.Reportable { // Filtering out TxError only if chain's log-node-errors = true. if _, ok := reportable.(*types.TxError); ok { - if !f.Chain.LogNodeErrors { - f.MetricsManager.LogFilteredEvent(f.Chain.Name, reportable.Type()) + if !subscription.LogNodeErrors { + f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) f.Logger.Debug().Msg("Got transaction error, skipping as node errors logging is disabled") return nil } - f.MetricsManager.LogMatchedEvent(f.Chain.Name, reportable.Type()) + f.MetricsManager.LogMatchedEvent(subscription.Name, reportable.Type()) return reportable } if _, ok := reportable.(*types.NodeConnectError); ok { - if !f.Chain.LogNodeErrors { - f.MetricsManager.LogFilteredEvent(f.Chain.Name, reportable.Type()) + if !subscription.LogNodeErrors { + f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) f.Logger.Debug().Msg("Got node error, skipping as node errors logging is disabled") return nil } - f.MetricsManager.LogMatchedEvent(f.Chain.Name, reportable.Type()) + f.MetricsManager.LogMatchedEvent(subscription.Name, reportable.Type()) return reportable } tx, ok := reportable.(*types.Tx) if !ok { f.Logger.Error().Str("type", reportable.Type()).Msg("Unsupported reportable type, ignoring.") - f.MetricsManager.LogFilteredEvent(f.Chain.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) return nil } - if !f.Chain.LogFailedTransactions && tx.Code > 0 { + if !subscription.LogFailedTransactions && tx.Code > 0 { f.Logger.Debug(). Str("hash", tx.GetHash()). Msg("Transaction is failed, skipping") - f.MetricsManager.LogFilteredEvent(f.Chain.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) return nil } @@ -78,23 +113,24 @@ func (f *Filterer) Filter(reportable types.Reportable) types.Reportable { f.Logger.Fatal().Err(err).Msg("Error converting height to int64") } - if f.lastBlockHeight != 0 && f.lastBlockHeight > txHeight { + chainLastBlockHeight, ok := f.lastBlockHeights[chain.Name] + if ok && chainLastBlockHeight > txHeight { f.Logger.Debug(). Str("hash", tx.GetHash()). Int64("height", txHeight). - Int64("last_height", f.lastBlockHeight). + Int64("last_height", chainLastBlockHeight). Msg("Transaction height is less than the last one received, skipping") return nil } - if f.lastBlockHeight == 0 || f.lastBlockHeight < txHeight { - f.lastBlockHeight = txHeight + if !ok || chainLastBlockHeight < txHeight { + f.lastBlockHeights[chain.Name] = txHeight } messages := make([]types.Message, 0) for _, message := range tx.Messages { - filteredMessage := f.FilterMessage(message, false) + filteredMessage := f.FilterMessage(message, subscription, false) if filteredMessage != nil { messages = append(messages, filteredMessage) } @@ -104,18 +140,22 @@ func (f *Filterer) Filter(reportable types.Reportable) types.Reportable { f.Logger.Debug(). Str("hash", tx.GetHash()). Msg("All messages in transaction were filtered out, skipping.") - f.MetricsManager.LogFilteredEvent(f.Chain.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) return nil } tx.Messages = messages - f.MetricsManager.LogMatchedEvent(f.Chain.Name, reportable.Type()) + f.MetricsManager.LogMatchedEvent(subscription.Name, reportable.Type()) return tx } -func (f *Filterer) FilterMessage(message types.Message, internal bool) types.Message { +func (f *Filterer) FilterMessage( + message types.Message, + subscription *configTypes.Subscription, + internal bool, +) types.Message { if unsupportedMsg, ok := message.(*messagesPkg.MsgUnsupportedMessage); ok { - if f.Chain.LogUnknownMessages { + if subscription.LogUnknownMessages { f.Logger.Error().Str("type", unsupportedMsg.MsgType).Msg("Unsupported message type") return message } else { @@ -125,7 +165,7 @@ func (f *Filterer) FilterMessage(message types.Message, internal bool) types.Mes } if unparsedMsg, ok := message.(*messagesPkg.MsgUnparsedMessage); ok { - if f.Chain.LogUnparsedMessages { + if subscription.LogUnparsedMessages { f.Logger.Error().Err(unparsedMsg.Error).Str("type", unparsedMsg.MsgType).Msg("Error parsing message") return message } @@ -137,15 +177,15 @@ func (f *Filterer) FilterMessage(message types.Message, internal bool) types.Mes return nil } - // internal -> filter only if f.Chain.FilterInternalMessages is true + // internal -> filter only if subscription.FilterInternalMessages is true // !internal -> filter regardless - if !internal || f.Chain.FilterInternalMessages { - matches, err := f.Chain.Filters.Matches(message.GetValues()) + if !internal || subscription.FilterInternalMessages { + matches, err := subscription.Filters.Matches(message.GetValues()) f.Logger.Trace(). Str("type", message.Type()). Str("values", fmt.Sprintf("%+v", message.GetValues().ToMap())). - Str("filters", fmt.Sprintf("%+v", f.Chain.Filters)). + Str("filters", fmt.Sprintf("%+v", subscription.Filters)). Bool("matches", matches). Msg("Result of matching message events against filters") @@ -170,7 +210,7 @@ func (f *Filterer) FilterMessage(message types.Message, internal bool) types.Mes // Processing internal messages (such as ones in MsgExec) for _, internalMessage := range message.GetParsedMessages() { - if internalMessageParsed := f.FilterMessage(internalMessage, true); internalMessageParsed != nil { + if internalMessageParsed := f.FilterMessage(internalMessage, subscription, true); internalMessageParsed != nil { parsedInternalMessages = append(parsedInternalMessages, internalMessageParsed) } } diff --git a/pkg/reporters/reporter.go b/pkg/reporters/reporter.go index f271f3c..4dccc8e 100644 --- a/pkg/reporters/reporter.go +++ b/pkg/reporters/reporter.go @@ -20,6 +20,18 @@ type Reporter interface { Send(report types.Report) error } +type Reporters []Reporter + +func (r Reporters) FindByName(name string) Reporter { + for _, reporter := range r { + if reporter.Name() == name { + return reporter + } + } + + return nil +} + func GetReporter( reporterConfig *configTypes.Reporter, appConfig *config.AppConfig, diff --git a/pkg/types/report.go b/pkg/types/report.go index ca46e81..73269e0 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -5,7 +5,8 @@ import ( ) type Report struct { - Chain types.Chain - Node string - Reportable Reportable + Chain types.Chain + Subscription types.Subscription + Node string + Reportable Reportable } diff --git a/templates/telegram/Tx.html b/templates/telegram/Tx.html index 63c8ea5..d85ef94 100644 --- a/templates/telegram/Tx.html +++ b/templates/telegram/Tx.html @@ -1,14 +1,12 @@ 💸 New transaction on chain {{ .Chain.GetName }} Hash: {{ SerializeLink .Reportable.Hash }} Height: {{ SerializeLink .Reportable.Height }} -{{- if .Chain.LogFailedTransactions }} {{- if not .Reportable.Code }} Status: 👌 Success {{- else }} Status: ❌ Failure Error: {{ .Reportable.Log }} {{- end }} -{{- end }} {{- if .Reportable.Memo }} Memo: {{ .Reportable.Memo }} {{- end }} From fb7b2e725dc8eea58b4761286929b1e2ea517035 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 00:17:33 +0300 Subject: [PATCH 02/12] feat: add subscription to report --- pkg/app.go | 11 ++--------- pkg/filterer/filterer.go | 22 +++++++++++----------- pkg/types/report.go | 2 +- templates/telegram/Tx.html | 2 ++ 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/pkg/app.go b/pkg/app.go index 8837133..02f3cfb 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -2,7 +2,6 @@ package pkg import ( configTypes "main/pkg/config/types" - "main/pkg/types" "os" "os/signal" "syscall" @@ -97,7 +96,7 @@ func (a *App) Start() { case rawReport := <-a.NodesManager.Channel: fetcher, _ := a.DataFetchers[rawReport.Chain.Name] - reportablesForReporters := a.Filterer.GetReportableForReporters(rawReport.Reportable) + reportablesForReporters := a.Filterer.GetReportableForReporters(rawReport) if len(reportablesForReporters) == 0 { a.Logger.Debug(). @@ -108,13 +107,7 @@ func (a *App) Start() { continue } - for reporterName, reportable := range reportablesForReporters { - report := types.Report{ - Node: rawReport.Node, - Chain: rawReport.Chain, - Reportable: reportable, - } - + for reporterName, report := range reportablesForReporters { a.Logger.Info(). Str("node", report.Node). Str("chain", report.Chain.Name). diff --git a/pkg/filterer/filterer.go b/pkg/filterer/filterer.go index a4786d3..e414a43 100644 --- a/pkg/filterer/filterer.go +++ b/pkg/filterer/filterer.go @@ -35,30 +35,30 @@ func NewFilterer( } func (f *Filterer) GetReportableForReporters( - reportable types.Reportable, -) map[string]types.Reportable { - reportables := make(map[string]types.Reportable) + report types.Report, +) map[string]types.Report { + reportables := make(map[string]types.Report) for _, subscription := range f.Config.Subscriptions { chain := f.Config.Chains.FindByName(subscription.Chain) reportableFiltered := f.FilterForChainAndSubscription( - reportable, + report.Reportable, chain, subscription, ) if reportableFiltered != nil { f.Logger.Info(). - Str("type", reportable.Type()). + Str("type", report.Reportable.Type()). Str("subscription_name", subscription.Name). Msg("Got report for subscription") - reportables[subscription.Reporter] = reportableFiltered - } else { - f.Logger.Info(). - Str("type", reportable.Type()). - Str("subscription_name", subscription.Name). - Msg("No report for subscription") + reportables[subscription.Reporter] = types.Report{ + Chain: report.Chain, + Node: report.Node, + Reportable: reportableFiltered, + Subscription: subscription, + } } } diff --git a/pkg/types/report.go b/pkg/types/report.go index 73269e0..e251ca7 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -6,7 +6,7 @@ import ( type Report struct { Chain types.Chain - Subscription types.Subscription + Subscription *types.Subscription Node string Reportable Reportable } diff --git a/templates/telegram/Tx.html b/templates/telegram/Tx.html index d85ef94..ea3e789 100644 --- a/templates/telegram/Tx.html +++ b/templates/telegram/Tx.html @@ -1,12 +1,14 @@ 💸 New transaction on chain {{ .Chain.GetName }} Hash: {{ SerializeLink .Reportable.Hash }} Height: {{ SerializeLink .Reportable.Height }} +{{- if .Subscription.LogFailedTransactions }} {{- if not .Reportable.Code }} Status: 👌 Success {{- else }} Status: ❌ Failure Error: {{ .Reportable.Log }} {{- end }} +{{- end }} {{- if .Reportable.Memo }} Memo: {{ .Reportable.Memo }} {{- end }} From 9052917a63d02e8b777f6b5630c35429000f11e8 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 00:20:04 +0300 Subject: [PATCH 03/12] chore: fixed linting --- pkg/config/toml_config/subscription.go | 13 +++++++------ pkg/config/toml_config/toml_config.go | 2 +- pkg/converter/converter.go | 4 ---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pkg/config/toml_config/subscription.go b/pkg/config/toml_config/subscription.go index 286c9d0..2cabd16 100644 --- a/pkg/config/toml_config/subscription.go +++ b/pkg/config/toml_config/subscription.go @@ -2,9 +2,10 @@ package toml_config import ( "fmt" + "main/pkg/config/types" + "github.com/cometbft/cometbft/libs/pubsub/query" "gopkg.in/guregu/null.v4" - "main/pkg/config/types" ) type Subscriptions []*Subscription @@ -14,11 +15,11 @@ type Subscription struct { Reporter string `toml:"reporter"` Chain string `toml:"chain"` Filters []string `toml:"filters"` - LogUnknownMessages null.Bool `default:"false" toml:"log-unknown-messages"` - LogUnparsedMessages null.Bool `default:"true" toml:"log-unparsed-messages"` - LogFailedTransactions null.Bool `default:"true" toml:"log-failed-transactions"` - LogNodeErrors null.Bool `default:"true" toml:"log-node-errors"` - FilterInternalMessages null.Bool `default:"true" toml:"filter-internal-messages"` + LogUnknownMessages null.Bool `default:"false" toml:"log-unknown-messages"` + LogUnparsedMessages null.Bool `default:"true" toml:"log-unparsed-messages"` + LogFailedTransactions null.Bool `default:"true" toml:"log-failed-transactions"` + LogNodeErrors null.Bool `default:"true" toml:"log-node-errors"` + FilterInternalMessages null.Bool `default:"true" toml:"filter-internal-messages"` } func (subscriptions Subscriptions) Validate() error { diff --git a/pkg/config/toml_config/toml_config.go b/pkg/config/toml_config/toml_config.go index d935789..c340719 100644 --- a/pkg/config/toml_config/toml_config.go +++ b/pkg/config/toml_config/toml_config.go @@ -13,7 +13,7 @@ type TomlConfig struct { MetricsConfig MetricsConfig `toml:"metrics"` Chains Chains `toml:"chains"` Subscriptions Subscriptions `toml:"subscriptions"` - Timezone string `default:"Etc/GMT" toml:"timezone"` + Timezone string `default:"Etc/GMT" toml:"timezone"` Reporters Reporters `toml:"reporters"` } diff --git a/pkg/converter/converter.go b/pkg/converter/converter.go index 91fb27d..f88317f 100644 --- a/pkg/converter/converter.go +++ b/pkg/converter/converter.go @@ -84,10 +84,6 @@ func (c *Converter) ParseEvent(event jsonRpcTypes.RPCResponse, nodeURL string) t return nil } - //c.Logger.Trace(). - // Str("values", fmt.Sprintf("%+v", resultEvent.Events)). - // Msg("Event values") - eventDataTx, ok := resultEvent.Data.(tendermintTypes.EventDataTx) if !ok { c.Logger.Debug().Msg("Could not convert tx result to EventDataTx.") From dbc0e8cd52f41dec875fd69369477e3b368cc9c5 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 00:53:00 +0300 Subject: [PATCH 04/12] chore: use default query --- pkg/config/toml_config/chain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/toml_config/chain.go b/pkg/config/toml_config/chain.go index 12cb57a..cde8053 100644 --- a/pkg/config/toml_config/chain.go +++ b/pkg/config/toml_config/chain.go @@ -12,7 +12,7 @@ type Chain struct { PrettyName string `toml:"pretty-name"` TendermintNodes []string `toml:"tendermint-nodes"` APINodes []string `toml:"api-nodes"` - Queries []string `toml:"queries"` + Queries []string `default:"[\"tx.height > 1\"]" toml:"queries"` MintscanPrefix string `toml:"mintscan-prefix"` PingPrefix string `toml:"ping-prefix"` PingBaseUrl string `default:"https://ping.pub" toml:"ping-base-url"` From 521a653dac8830d925f9a85ef21faa5523f5569b Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 01:57:34 +0300 Subject: [PATCH 05/12] feat: skip subscription if chain is different --- pkg/config/toml_config/subscription.go | 61 ++++++++++++++++++----- pkg/config/toml_config/toml_config.go | 17 +++++-- pkg/config/types/subscription.go | 14 ++++-- pkg/filterer/filterer.go | 68 +++++++++++++++----------- pkg/types/report.go | 8 +-- templates/telegram/Tx.html | 2 +- 6 files changed, 119 insertions(+), 51 deletions(-) diff --git a/pkg/config/toml_config/subscription.go b/pkg/config/toml_config/subscription.go index 2cabd16..3d91598 100644 --- a/pkg/config/toml_config/subscription.go +++ b/pkg/config/toml_config/subscription.go @@ -11,9 +11,15 @@ import ( type Subscriptions []*Subscription type Subscription struct { - Name string `toml:"name"` - Reporter string `toml:"reporter"` - Chain string `toml:"chain"` + Name string `toml:"name"` + Reporter string `toml:"reporter"` + ChainSubscription ChainSubscriptions `toml:"chains"` +} + +type ChainSubscriptions []*ChainSubscription + +type ChainSubscription struct { + Chain string `toml:"name"` Filters []string `toml:"filters"` LogUnknownMessages null.Bool `default:"false" toml:"log-unknown-messages"` LogUnparsedMessages null.Bool `default:"true" toml:"log-unparsed-messages"` @@ -52,6 +58,16 @@ func (s *Subscription) Validate() error { return fmt.Errorf("empty reporter name") } + for index, subscription := range s.ChainSubscription { + if err := subscription.Validate(); err != nil { + return fmt.Errorf("error in subscription %d: %s", index, err) + } + } + + return nil +} + +func (s *ChainSubscription) Validate() error { if s.Chain == "" { return fmt.Errorf("empty chain name") } @@ -65,15 +81,13 @@ func (s *Subscription) Validate() error { return nil } -func (s *Subscription) ToAppConfigSubscription() *types.Subscription { +func (s *ChainSubscription) ToAppConfigChainSubscription() *types.ChainSubscription { filters := make([]query.Query, len(s.Filters)) for index, filter := range s.Filters { filters[index] = *query.MustParse(filter) } - return &types.Subscription{ - Name: s.Name, - Reporter: s.Reporter, + return &types.ChainSubscription{ Chain: s.Chain, Filters: filters, LogUnknownMessages: s.LogUnknownMessages.Bool, @@ -84,10 +98,21 @@ func (s *Subscription) ToAppConfigSubscription() *types.Subscription { } } -func FromAppConfigSubscription(s *types.Subscription) *Subscription { - subscription := &Subscription{ - Name: s.Name, - Reporter: s.Reporter, +func (s *Subscription) ToAppConfigSubscription() *types.Subscription { + chainSubscriptions := make(types.ChainSubscriptions, len(s.ChainSubscription)) + for index, chainSubscription := range s.ChainSubscription { + chainSubscriptions[index] = chainSubscription.ToAppConfigChainSubscription() + } + + return &types.Subscription{ + Name: s.Name, + Reporter: s.Reporter, + ChainSubscriptions: chainSubscriptions, + } +} + +func FromAppConfigChainSubscription(s *types.ChainSubscription) *ChainSubscription { + subscription := &ChainSubscription{ Chain: s.Chain, LogUnknownMessages: null.BoolFrom(s.LogUnknownMessages), LogUnparsedMessages: null.BoolFrom(s.LogUnparsedMessages), @@ -103,3 +128,17 @@ func FromAppConfigSubscription(s *types.Subscription) *Subscription { return subscription } + +func FromAppConfigSubscription(s *types.Subscription) *Subscription { + subscription := &Subscription{ + Name: s.Name, + Reporter: s.Reporter, + ChainSubscription: make(ChainSubscriptions, len(s.ChainSubscriptions)), + } + + for index, chainSubscription := range s.ChainSubscriptions { + subscription.ChainSubscription[index] = FromAppConfigChainSubscription(chainSubscription) + } + + return subscription +} diff --git a/pkg/config/toml_config/toml_config.go b/pkg/config/toml_config/toml_config.go index c340719..b075985 100644 --- a/pkg/config/toml_config/toml_config.go +++ b/pkg/config/toml_config/toml_config.go @@ -45,12 +45,23 @@ func (c *TomlConfig) Validate() error { } for index, subscription := range c.Subscriptions { - if !c.Chains.HasChainByName(subscription.Chain) { - return fmt.Errorf("error in subscription %d: no such chain '%s'", index, subscription.Chain) + for chainSubscriptionIndex, chainSubscription := range subscription.ChainSubscription { + if !c.Chains.HasChainByName(chainSubscription.Chain) { + return fmt.Errorf( + "error in subscription %d: error in chain %d: no such chain '%s'", + index, + chainSubscriptionIndex, + chainSubscription.Chain, + ) + } } if !c.Reporters.HasReporterByName(subscription.Reporter) { - return fmt.Errorf("error in subscription %d: no such chain '%s'", index, subscription.Chain) + return fmt.Errorf( + "error in subscription %d: no such reporter '%s'", + index, + subscription.Reporter, + ) } } diff --git a/pkg/config/types/subscription.go b/pkg/config/types/subscription.go index 65e8b73..787f2fe 100644 --- a/pkg/config/types/subscription.go +++ b/pkg/config/types/subscription.go @@ -3,10 +3,16 @@ package types type Subscriptions []*Subscription type Subscription struct { - Name string - Chain string - Reporter string - Filters Filters + Name string + Reporter string + ChainSubscriptions ChainSubscriptions +} + +type ChainSubscriptions []*ChainSubscription + +type ChainSubscription struct { + Chain string + Filters Filters LogUnknownMessages bool LogUnparsedMessages bool diff --git a/pkg/filterer/filterer.go b/pkg/filterer/filterer.go index e414a43..e3d71c4 100644 --- a/pkg/filterer/filterer.go +++ b/pkg/filterer/filterer.go @@ -40,24 +40,33 @@ func (f *Filterer) GetReportableForReporters( reportables := make(map[string]types.Report) for _, subscription := range f.Config.Subscriptions { - chain := f.Config.Chains.FindByName(subscription.Chain) - - reportableFiltered := f.FilterForChainAndSubscription( - report.Reportable, - chain, - subscription, - ) - - if reportableFiltered != nil { - f.Logger.Info(). - Str("type", report.Reportable.Type()). - Str("subscription_name", subscription.Name). - Msg("Got report for subscription") - reportables[subscription.Reporter] = types.Report{ - Chain: report.Chain, - Node: report.Node, - Reportable: reportableFiltered, - Subscription: subscription, + for _, chainSubscription := range subscription.ChainSubscriptions { + if chainSubscription.Chain != report.Chain.Name { + continue + } + + chain := f.Config.Chains.FindByName(chainSubscription.Chain) + + reportableFiltered := f.FilterForChainAndSubscription( + report.Reportable, + chain, + subscription, + chainSubscription, + ) + + if reportableFiltered != nil { + f.Logger.Info(). + Str("type", report.Reportable.Type()). + Str("chain", chain.Name). + Str("hash", report.Reportable.GetHash()). + Str("subscription_name", subscription.Name). + Msg("Got report for subscription") + reportables[subscription.Reporter] = types.Report{ + Chain: report.Chain, + Node: report.Node, + Reportable: reportableFiltered, + ChainSubscription: chainSubscription, + } } } } @@ -69,10 +78,11 @@ func (f *Filterer) FilterForChainAndSubscription( reportable types.Reportable, chain *configTypes.Chain, subscription *configTypes.Subscription, + chainSubscription *configTypes.ChainSubscription, ) types.Reportable { // Filtering out TxError only if chain's log-node-errors = true. if _, ok := reportable.(*types.TxError); ok { - if !subscription.LogNodeErrors { + if !chainSubscription.LogNodeErrors { f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) f.Logger.Debug().Msg("Got transaction error, skipping as node errors logging is disabled") return nil @@ -83,7 +93,7 @@ func (f *Filterer) FilterForChainAndSubscription( } if _, ok := reportable.(*types.NodeConnectError); ok { - if !subscription.LogNodeErrors { + if !chainSubscription.LogNodeErrors { f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) f.Logger.Debug().Msg("Got node error, skipping as node errors logging is disabled") return nil @@ -100,7 +110,7 @@ func (f *Filterer) FilterForChainAndSubscription( return nil } - if !subscription.LogFailedTransactions && tx.Code > 0 { + if !chainSubscription.LogFailedTransactions && tx.Code > 0 { f.Logger.Debug(). Str("hash", tx.GetHash()). Msg("Transaction is failed, skipping") @@ -116,6 +126,7 @@ func (f *Filterer) FilterForChainAndSubscription( chainLastBlockHeight, ok := f.lastBlockHeights[chain.Name] if ok && chainLastBlockHeight > txHeight { f.Logger.Debug(). + Str("chain", chainSubscription.Chain). Str("hash", tx.GetHash()). Int64("height", txHeight). Int64("last_height", chainLastBlockHeight). @@ -130,7 +141,7 @@ func (f *Filterer) FilterForChainAndSubscription( messages := make([]types.Message, 0) for _, message := range tx.Messages { - filteredMessage := f.FilterMessage(message, subscription, false) + filteredMessage := f.FilterMessage(message, subscription, chainSubscription, false) if filteredMessage != nil { messages = append(messages, filteredMessage) } @@ -152,10 +163,11 @@ func (f *Filterer) FilterForChainAndSubscription( func (f *Filterer) FilterMessage( message types.Message, subscription *configTypes.Subscription, + chainSubscription *configTypes.ChainSubscription, internal bool, ) types.Message { if unsupportedMsg, ok := message.(*messagesPkg.MsgUnsupportedMessage); ok { - if subscription.LogUnknownMessages { + if chainSubscription.LogUnknownMessages { f.Logger.Error().Str("type", unsupportedMsg.MsgType).Msg("Unsupported message type") return message } else { @@ -165,7 +177,7 @@ func (f *Filterer) FilterMessage( } if unparsedMsg, ok := message.(*messagesPkg.MsgUnparsedMessage); ok { - if subscription.LogUnparsedMessages { + if chainSubscription.LogUnparsedMessages { f.Logger.Error().Err(unparsedMsg.Error).Str("type", unparsedMsg.MsgType).Msg("Error parsing message") return message } @@ -179,13 +191,13 @@ func (f *Filterer) FilterMessage( // internal -> filter only if subscription.FilterInternalMessages is true // !internal -> filter regardless - if !internal || subscription.FilterInternalMessages { - matches, err := subscription.Filters.Matches(message.GetValues()) + if !internal || chainSubscription.FilterInternalMessages { + matches, err := chainSubscription.Filters.Matches(message.GetValues()) f.Logger.Trace(). Str("type", message.Type()). Str("values", fmt.Sprintf("%+v", message.GetValues().ToMap())). - Str("filters", fmt.Sprintf("%+v", subscription.Filters)). + Str("filters", fmt.Sprintf("%+v", chainSubscription.Filters)). Bool("matches", matches). Msg("Result of matching message events against filters") @@ -210,7 +222,7 @@ func (f *Filterer) FilterMessage( // Processing internal messages (such as ones in MsgExec) for _, internalMessage := range message.GetParsedMessages() { - if internalMessageParsed := f.FilterMessage(internalMessage, subscription, true); internalMessageParsed != nil { + if internalMessageParsed := f.FilterMessage(internalMessage, subscription, chainSubscription, true); internalMessageParsed != nil { parsedInternalMessages = append(parsedInternalMessages, internalMessageParsed) } } diff --git a/pkg/types/report.go b/pkg/types/report.go index e251ca7..4a7b975 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -5,8 +5,8 @@ import ( ) type Report struct { - Chain types.Chain - Subscription *types.Subscription - Node string - Reportable Reportable + Chain types.Chain + ChainSubscription *types.ChainSubscription + Node string + Reportable Reportable } diff --git a/templates/telegram/Tx.html b/templates/telegram/Tx.html index ea3e789..ea6af40 100644 --- a/templates/telegram/Tx.html +++ b/templates/telegram/Tx.html @@ -1,7 +1,7 @@ 💸 New transaction on chain {{ .Chain.GetName }} Hash: {{ SerializeLink .Reportable.Hash }} Height: {{ SerializeLink .Reportable.Height }} -{{- if .Subscription.LogFailedTransactions }} +{{- if .ChainSubscription.LogFailedTransactions }} {{- if not .Reportable.Code }} Status: 👌 Success {{- else }} From 547cc960f6303abaf92beca7436148d33409dcf7 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 01:58:40 +0300 Subject: [PATCH 06/12] chore: make readme lines shorter --- README.md | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f307424..33a841b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ ![Latest release](https://img.shields.io/github/v/release/QuokkaStake/cosmos-transactions-bot) [![Actions Status](https://github.com/QuokkaStake/cosmos-transactions-bot/workflows/test/badge.svg)](https://github.com/QuokkaStake/cosmos-transactions-bot/actions) -cosmos-transactions-bot is a tool that listens to transactions with a specific filter on multiple chains and reports them to a Telegram channel. +cosmos-transactions-bot is a tool that listens to transactions with a specific filter on multiple chains +and reports them to a Telegram channel. Here's how it may look like: @@ -31,7 +32,8 @@ Then we need to create a systemd service for our app: sudo nano /etc/systemd/system/cosmos-transactions-bot.service ``` -You can use this template (change the user to whatever user you want this to be executed from. It's advised to create a separate user for that instead of running it from root): +You can use this template (change the user to whatever user you want this to be executed from. It's advised +to create a separate user for that instead of running it from root): ``` [Unit] @@ -69,29 +71,44 @@ sudo journalctl -u cosmos-transactions-bot -f --output cat ## How does it work? -There are multiple nodes this app is connecting to via Websockets (see [this](https://docs.tendermint.com/master/rpc/#/Websocket/subscribe) for more details) and subscribing to the queries that are set in config. When a new transaction matching the filters is found, it's put through a deduplication filter first, to make sure we don't send the same transaction twice. Then each message in transaction is enriched (for example, if someone claims rewards, the app fetches Coingecko price and validator rewards are claimed from). Lastly, each of these transactions are sent to a reporter (currently Telegram only) to notify those who need it. +There are multiple nodes this app is connecting to via Websockets (see [this](https://docs.tendermint.com/master/rpc/#/Websocket/subscribe) for more details) and subscribing +to the queries that are set in config. When a new transaction matching the filters is found, it's put through +a deduplication filter first, to make sure we don't send the same transaction twice. Then each message +in transaction is enriched (for example, if someone claims rewards, the app fetches Coingecko price +and validator rewards are claimed from). Lastly, each of these transactions are sent to a reporter +(currently Telegram only) to notify those who need it. ## How can I configure it? -All configuration is done with a `.toml` file, which is passed to an app through a `--config` flag. See `config.example.toml` for reference. +All configuration is done with a `.toml` file, which is passed to an app through a `--config` flag. +See `config.example.toml` for reference. ### Queries and filters This is quite complex and deserves a special explanation. -When a node starts, it connects to a Websocket of the fullnode and subscribes to queries (`queries` in `.toml` config). If there's a transaction that does not match these filters, a fullnode won't emit the event for it and this transaction won't reach the app. +When a node starts, it connects to a Websocket of the fullnode and subscribes to queries (`queries` in `.toml` config). +If there's a transaction that does not match these filters, a fullnode won't emit the event for it +and this transaction won't reach the app. -If using filters (`filters` in `.toml` config), when a transaction is received, all messages in the transaction are checked whether they match these filters, and can be filtered out (and the transaction itself would be filtered out if there are 0 non filtered messages left). +If using filters (`filters` in `.toml` config), when a transaction is received, all messages in the transaction +are checked whether they match these filters, and can be filtered out (and the transaction itself would be filtered out +if there are 0 non-filtered messages left). -Using filters can be useful is you have transactions with multiple messages, where you only need to know about one (for example, someone claiming rewards from your validator and other ones, when you need to know only about claiming from your validator). +Using filters can be useful is you have transactions with multiple messages, where you only need to know about one +(for example, someone claiming rewards from your validator and other ones, when you need to know only about claiming +from your validator). Filters should follow the same pattern as queries, but they can only match the following pattern (so no AND/OR support): - `xxx = yyy` (which would filter the transaction if key doesn't match value) - `xxx! = yyy` (which would filter the transaction if key does match value) -Please note that the message would not be filtered out if it matches at least one filter. Example: you have a message that has `xxx = yyy` as events, and if using `xxx != yyy` and `xxx != zzz` as filters, it won't get filtered out (as it would not match the first filter but would match the second one). +Please note that the message would not be filtered out if it matches at least one filter. +Example: you have a message that has `xxx = yyy` as events, and if using `xxx != yyy` and `xxx != zzz` as filters, +it won't get filtered out (as it would not match the first filter but would match the second one). -You can always use `tx.height > 0`, which will send you the information on all transactions in chain, or check out something we have: +You can always use `tx.height > 0`, which will send you the information on all transactions in chain, +or check out something we have: ``` @@ -135,7 +152,10 @@ filters = [ See [the documentation](https://docs.tendermint.com/master/rpc/#/Websocket/subscribe) for more information on queries. -One important thing to keep in mind: by default, Tendermint RPC now only allows 5 connections per client, so if you have more than 5 filters specified, this will fail when subscribing to 6th one. If you own the node you are subscribing to, o fix this, change this parameter to something that suits your needs in `/config/config.toml`: +One important thing to keep in mind: by default, Tendermint RPC now only allows 5 connections per client, +so if you have more than 5 filters specified, this will fail when subscribing to 6th one. +If you own the node you are subscribing to, o fix this, change this parameter to something that suits your needs +in `/config/config.toml`: ``` max_subscriptions_per_client = 5 @@ -144,12 +164,16 @@ max_subscriptions_per_client = 5 ## Notifications channels Go to [@BotFather](https://t.me/BotFather) in Telegram and create a bot. After that, there are two options: -- you want to send messages to a user. This user should write a message to [@getmyid_bot](https://t.me/getmyid_bot), then copy the `Your user ID` number. Also keep in mind that the bot won't be able to send messages unless you contact it first, so write a message to a bot before proceeding. -- you want to send messages to a channel. Write something to a channel, then forward it to [@getmyid_bot](https://t.me/getmyid_bot) and copy the `Forwarded from chat` number. Then add the bot as an admin. +- you want to send messages to a user. This user should write a message to [@getmyid_bot](https://t.me/getmyid_bot), +then copy the `Your user ID` number. Also keep in mind that the bot won't be able to send messages +unless you contact it first, so write a message to a bot before proceeding. +- you want to send messages to a channel. Write something to a channel, then forward it to [@getmyid_bot](https://t.me/getmyid_bot) +and copy the `Forwarded from chat` number. Then add the bot as an admin. Then run a program with Telegram config (see `config.example.toml` as example). -You would likely want to also put only the IDs of trusted people to admins list in Telegram config, so the bot won't react to anyone writing messages to it except these users. +You would likely want to also put only the IDs of trusted people to admins list in Telegram config, so the bot +won't react to anyone writing messages to it except these users. Additionally, for the ease of using commands, you can put the following list as bot commands in @BotFather settings: From dab2603986e936d67463db17c0f18749727028ca Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 02:35:33 +0300 Subject: [PATCH 07/12] chore: add README for the new schema --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++-- images/schema.png | Bin 0 -> 54100 bytes 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 images/schema.png diff --git a/README.md b/README.md index 33a841b..c39c74d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ and reports them to a Telegram channel. Here's how it may look like: -![Telegram](https://raw.githubusercontent.com/QuokkaStake/cosmos-transactions-bot/master/images/telegram.png) +![Telegram](https://raw.githubusercontent.com/QuokkaStake/cosmos-transactions-bot/main/images/telegram.png) ## How can I set it up? @@ -83,9 +83,62 @@ and validator rewards are claimed from). Lastly, each of these transactions are All configuration is done with a `.toml` file, which is passed to an app through a `--config` flag. See `config.example.toml` for reference. +### Chains, subscriptions, chain subscriptions and reporters + +This app's design is quite complex to allow it to be as flexible as possible. +There are the main objects that this app has: + +- reporter - something that acts as a destination point (e.g. Telegram bot) and maybe allows you as a user +to interact with it in a special way (like, setting aliases etc.) +- chain - info about chain itself, its denoms, queries (see below), nodes used to receive data from, etc. +- subscription - info about which set of chains and their events to send to which reporter, +has many chain subscriptions +- chain subscription - info about which chain to receive data from, filters on which events to match +(see below) and how to process errors/unparsed/unsupported messages, if any. + +Each chain has many chain subscriptions, each subscription has one reporter, each chain subscription +has one chain and many filters. + +Generally speaking, the workflow of the app looks something like this: + +![Schema](https://raw.githubusercontent.com/QuokkaStake/cosmos-transactions-bot/main/images/schema.png) + +This allows to build very flexible setups. Here's the example of the easy and the more difficult setup. + +1) "I want to receive all transactions sent from my wallet on chain A, B and C to my Telegram channel" + +You can do it the following way: +- have 1 reporter, a Telegram channel +- have 3 chains, A, B and C, and their configs +- have 1 subscription, with Telegram reporter and 3 chain subscriptions inside (one for chain A, B and C +with 1 filter each matching transfers from wallets on these chains) + +2) "I want to receive all transactions sent from my wallet on chains A, B and C to one Telegram chat, +all transactions that are votes on chains A and B to another Telegram chat, and all transactions that are delegations +with amount more than 10M $TOKEN on chain C to another Telegram chat" + +That's also manageable. You can do the following: +- reporter 1, "first", a bot that sends messages to Telegram channel 1 +- reporter 2, "second", a bot that sends messages to Telegram channel 2 +- reporter 3, "third", a bot that sends messages to Telegram channel 3 +- chain A and its config +- chain B and its config +- chain C and its config +- subscription 1, let's call it "my-wallet-sends", with reporter "first" and the following chain subscriptions +- - chain subscription 1, chain A, 1 filter matching transfers from my wallet on chain A +- - chain subscription 2, chain B, 1 filter matching transfers from my wallet on chain B +- - chain subscription 3, chain C, 1 filter matching transfers from my wallet on chain C +- subscription 2, let's call it "all-wallet-votes", with reporter "second" and the following chain subscriptions +- - chain subscription 1, chain A, 1 filter matching any vote on chain A +- - chain subscription 2, chain B, 1 filter matching any vote on chain B +- subscription 3, let's call it "whale-votes", with reporter "third" and the following chain subscription +- - chain subscription 1, chain C, 1 filter matching any delegations with amount more than 10M $TOKEN on chain C + +See config.example.toml for real-life examples. + ### Queries and filters -This is quite complex and deserves a special explanation. +This is another quite complex topic and deserves a special explanation. When a node starts, it connects to a Websocket of the fullnode and subscribes to queries (`queries` in `.toml` config). If there's a transaction that does not match these filters, a fullnode won't emit the event for it @@ -99,6 +152,9 @@ Using filters can be useful is you have transactions with multiple messages, whe (for example, someone claiming rewards from your validator and other ones, when you need to know only about claiming from your validator). +Keep in mind that queries is set on the app level, while filters are set on a chain subscription level, +so you can have some generic query on a chain, and more granular filter on each of your chain subscriptions. + Filters should follow the same pattern as queries, but they can only match the following pattern (so no AND/OR support): - `xxx = yyy` (which would filter the transaction if key doesn't match value) - `xxx! = yyy` (which would filter the transaction if key does match value) diff --git a/images/schema.png b/images/schema.png new file mode 100644 index 0000000000000000000000000000000000000000..4274734397c8c933976a09d28425cf3af71be87c GIT binary patch literal 54100 zcmeEv2Ut`|x;7vx7!Yv;5l}=BBxeL9Ckc`f$s(~yP0rbXh=3?Ta*(8CXmS!ID2Paw zWFv^gCWtf$0)L(E#$jf>JF|Ot|DD;p&$H{&=hUfFRbP6)bT(L3S?=(GlLzqd@D9sg zl2*sV+vAOghyQ|TAGqR2aUur~Pr=q%=Bl%;yE)9p43B|F3i}rWH>V}s$(eyint_|! z*xsJq6lQGUXl(1mZfE8UE`j^Da8sB$%*+%!hMSX{myMH~jhjo8n}>l%f}0ck!^6qW z&&{Qa9dB%5W`~>bGTakpV`I#~Ez8Hw38rGYY-|FvbA~%vGVn-(-|}|OW;Wn|!DaBT ziU#=SD)=uKCp#A(c7&rVn4XK9o1ODKxFKg|VhsngK|{cQxWFY@n4_6F+#OuRjKVI# z%-PriyIi=6qm`^Om#4bD)dhJg4Vb5rln!=`tC^z{3~q9=UuO?{Gwf9h zINZX<47$oA!@wy4?m_?Ik;0u3_+e*kiyNy7w#^0>1kC`$ofTYO-owt?T3(Aso|9Wj zO6!V>jMcAa+nLkWUCzx5 z!Nz|vJ9I(A#26eLhN*T9(}tNk1Ik0Jc7a;}yB28lB{P_X<<6VA_yw_dY>jsY<96a? zX>1C2!#w3{l}bKE&iM>rhJw~J`IZ|@AUb(b=;f!J^79C7afy!7`!`+b{^-?{&P zGo3Q`b?vL#F4s+swK?tO)h{`z!Y$Zv2jgmNBi4qqCVK_Qaf=J$6pr5$D^%wLcUIEmXuhTlkm1rP&!?A0G|H%{XjU*8cL9IM$I z|I@ghmlrqZ4%-Xhe(ySOo}FvE*?#BRpNQ>o=lHYou>-d1H%I=@S3Ex!!vC#0;`=Qj zeyD$VaO3_wgy8uNAsD#%ZGgFxG=;f>KP{j?O)QOJc01RB2HbV)FGsQ++4+MM`}0{0 zzTX_VnJK_XIBN|IxFy^IZf9&G^Yc~7U&_(XW0c@ug!tuB5{0~uz zqnVAdGtBju%m0zg|B< zyI~76HHA=#q@$S=%oB%DAPL2>5TH1pBmO0cs zWaqhnT@;QpaR%#OB4R)D?T(Pa&CQ*5LKlCOS^wR9hx6!n`EP$Q1MzRS54a%7zu!JU zlJWcD12E>F>XYIGZkOW1X(`A_{uNvO<@6CoI{tBe#Ky_adw~z9+W)JBAgu61wT}~_ zT|D$3;o#wn(2gU%%kln;_v2 z(wYBfMB{&`!LhXe2YC9gc|Rp$;ZcMH^?k5D_S+(jttR(5I2KOI8g7W~0^|3whOPBi>)9r*tzZH1-%{|$ry zsZ~g`?mG6rX)ylk2;m>5?Oz=sfI75a6Z+3&dH%O_+0oa3d4$0I=SFh>WIFwo5yEew z+@B92{JOM;=Ld-FFH%~=jf?Q@vQ+=h&?`38`wxHwoFU=s%v%>(C-G#vGVZ`sD?ir zHUA@XLzWs8LrDF%zUD7hU&H;Q=LnHhcRIXqCU7!@#=}fWk=v_l{5f z`?hK~qQU=uk zI>yN*&ko}K`{d?-l*?bw&kc=)5|4l1{C_pM#3gh7400)e@s0k>a`{KiB{r4$f74w4 zlg0Aa=8|V8EAgK#mbld2f5j3g;NLA4|HI}Io4Wp2%HTuvMC-EOOIkYn&?s1|>yg-h(&&J4q@ishs!qX3)3V2h%KJxP2%L22|uASKP ztJM-P_>UgsEv(r4^ZGHbYiK#~>uoXN6B?xiG_n0^14N?Dzg#+AfH*Pj8o^;!yI<{J ze35J4uU15;fY(4_dClP<_ULds^~Vbg=lcFPcDuBy1VnY>dqVVnwEx6;=e zCa6PjZ_?DGn+hufDL?~+FX06oA{*T+ctfHg{V6eLQjToPEv!rg!~waplD)KtbWz2h z!gN~+#w7xt|MISz2d6S1sTSWyycn3!kaA7Em+(Y~*5h$WtU!ym1Es-tHXWa6jq%yR zV!uqpqf_|JPM&kqlvv@!s3h7Ogzge=uBmc9u>w;+?Q}SUTbel#&$y#qc!b}MjqV)~ z)6i84#~T+EL4P@f6;F)t-PT`T)B z=(KO44240@d-w6f*8TYgCARge@4fgvKAR5ttd5`>_Mb-hpa)t+H@|2Ul>Jm%L>D%h z)_k%aLjzhbZ~y}##N-ms?yd#ub2O}OEYuH;hl|-!!E1`xieJ*Thviq#SG9#*{Cvr4 zHm`iHjiVmXZ|K!7@{4%No$-M^{BbzF>3+t4bU83WmUc8ooQUJ)WRh3DfzL{B zCS{q_5s@Bc!HJ)|W}al(OubEYolJBSx~6h*>864Ehg_e}+?LPgO26soof5uAKC5o2Um|Lt=~{8uV&(>uo|^lXZ5 zEu#VtvKJ=%uD?uau- zcXrjNPtwDVZETY_;~SH=e4pX(zq-A(XYYh=X`Ro;paM&tX+ix~)vbAv>o7; z0-vQOio=RR1vQ@)`m>~wp0jzATlrh`^Yqk1<#708*-%Gx=qZsw-)~ED)&s?6W&5u_ z@0;+s$apVg9%tMPff&||rE?Ljow>hC@lnsNvI+gp<*DU|r#6E-!g={Xd)l{HZO;)vMrz9Xk>YzGkz zC3b3P#r8q@*D{JiG7I2%T}WH*QI4bWthSRLWak#QGNV5}RH8UCJq z7phH@Z$3q@$}hq))f82=NWCwIZ`Xudh`I6Q;V@hmPzH)Y!nKmuk_bO<1?Fzr4<5`S zJfEjqTFh2H+(qN|HcwALU?U?|ltHP}&)UO`eyur-#=1jfHQ|`eAWPP!$BN4-Kx6PR z)aZ>PuG_vJBjiq^7hh2fWYBYv<;IP4zn5gYEVI~D9kDE$H0~jfvzX%ifP0H6DRP-F z1k}{iTJKY7q4i$-5M(DmxjtL4!0lw^k&Ye_S$wuqu`+Mrd8O(GXUonhq)m#=B>4{X8d9IkDk)>>RMO9vtevQ|wKUFH|HbbIN z5WF?=eE!z+gB&(I7H^yhl-rceg3O5ohV7;Lx0)!7hH2L#`82)OB8hotCg-ac%9_aa za^)McCen%%KN}HvobG5O!&;I17QhBwlshtXgMsw`ht7M+qS|TX4tMTG2hUu+S=y&-9j1sdU7jtdcYR){-^rSY-h_1|6$e?J&n$M9 zmD$+lRb=AQ)kQF>$s3y%L_drk)XG;8-R@BDA1JZ7L4ZT90Y3qFt?sWJL&_xQPw~<5 z=F^RfWZn-MRr2%k^Pk?u8?R)x(I>>(!bM5EhRMBWL&cwQ;2YlG{CrPWRPV2J&l0Kgh z`%UcBJxX{D+B}jc{r&v_BG|A{2$?>5?P69-QW6dhVY~|NOW-6A?c4kC>A7d_`6p>8 zB(YOlfn&*`Jabt5nHY3_JQ+=2h&YqFO;kE?&H=_HqNIOz&~e|5KzXfW*Dh*@h8@IB z&389h&LkERtzAm%RF11(rd=0rkj-&V(IM^l&nRWuQopCIq zsxJ|3I^pm>RGYIs7YPpSodq@~6tD844FRk+dwss&&C6I$#P7YCNS#hOhu_hb>o}z6 z4di((H(N*i`?$T-2Vz|XF)xlKVQSrS3|OT0Tc!c-z{r8}D<<7*^O6;}@SNKHr@h~j2u;y$0P}ObW$xF=d~TLG z&;6Zo(7*wf+-q6MzMdH|Z%TaiH;`TnBJmeJic!FpoKpsNCERCyI?K=&md|ChlowN^ zQ(j=`+s%$UuY-V4I(G>8O`~x<+-t}exUh=!D{ndNH_ZRu-9*ncLS}}1)|0rd0*!R( zH*f`Za8M6kYi8v8HKP__lD_pNAi{PcDqSY3ZUfSqoekl9P; z6j~j2Le1n9@V6Jzcuu9hiOx#$Y`slFW99bVBWrdcm@%pFAV?lf2hCij9@htMpm zo=GTQu3`-P`~sM#Zp3@JeI8kCL^pq%%FdoATHw9=3!G}x667D8Klfw*fcaEmdy_VQg2$&=WlP1InOaXN z{a^zM^*CMg7BG@z2-B831D0V~Y!@^t4 zy4bK^xG00N5NJHXCHhXMrsLWdA~G^U$)teyl6dSA1p8qFCeA*rxPDDFBzwA1_u%FuA|H*dm6}@)PFH^?*Pr0WY%OXp$G6~pAUI-B~ zOay&(cBs4{57DLY#+SGmFkRcG?e^xX8{Jj3;*Wd}2Ake2>EuTWi%-OYg&knXy4ui_ ze60flo?k3CmRe5dnYBe94;^n_ih6r&|8W#7^4+i-(uc*W+hRhYCAp&xhKr`DIs>6j z3&;DG%!rjrJ(w5S**KAqn~xg%fNu-W$*+2P%m(SRns|(bOdxJ^<5N6*E=t?bq2!X^ z)^aimz-ecIbIn@-)9ME<0IHV)@bwxTl<1I2xXAEI#zeH9or9m>-|}qEEiwPduMyaT zej~hap@=zvsllzCufJlw^HiOBL)JjWEppwL#422BRKPxW1T zwe0#Xm)yF(w)o=6&=oyfv+HR(at?FvicDnOGRJZTq=Ne{O>F|#Isb6*yL2=e_%?0E zcmdty6Py9?e^!IE*!JtI9pD5AD-NeTS!`A4lqDtTo*;OYWz!gv;^B3b)7H{sa&7FT zJsKq@%y9!&tZNDnV=sRF_^@oiJic;PF(g{TYGA6g-*9$NJ?%vMeUbI4$4*3|?Rckr zzH}bu@dlo`&5gnb*HD9{IeO|(V+Gp#o?NQIaZ%nhAMk<`vFHGo z&$_c)u(<%=jdKxS2P~5L&acsrHJSnk)wTc939ECObVUx_R88_}E;kPIXHP} ze*H+U0exK>LsUlKLiXdZVxWHqQMTRp;>@QI+66_UEbX5M5=OhbRN7Of{m5d0?yfj| z>f*aW1Z>=VB#n}VSUtGXQa;P3_Dh#yV!KK6JqItmQa=@*@SNP@yHCW-mU`+-we!7J z=ap%v9}%NCYJmKXbg2^8S!U)f9ZLQd{LK&O%|s^6Z65R2mgxs-F?6!)c(ql@|&2oi1Jl;`Z7DlTKDBNa0U&3pMX~{Nrx(>`% z(U%c;2I_JwcT?~gwt=3{&q3^>ZyyQu6ruK$Yb?jT2M;oSpgVf5r~)vp5PrH@REF2u z7?CI{NCT;Gzw4;9e}VC4HQC5d4RKYSWSn+ zg(R7*u<@vOT!Y4^4$CVJAnfB7D_D9_9VH6XeVZ!v;(_u2$C=!9V0jAL z69l`7FR%$m(Zrn@wtsQhV59ZoNj3wmK2_8QjdO3aldBi%hET>f3X8DHY@32~UJ4^( zF%*TS*cr3wT<440&mUC%NbP4V_~zKE=x~+FU8Xyytd>SfoZPEzG{UCRQ&$Fz!C6X|5)}v`m|H>1#C|IUu7pz^Ct5J`v3lm(g7( z7JM~WyWNsm(Lxiktx|Hc^HlTH!r;T~bk1V!HhD!9bH7DGM_ZFNfzB(}F6N23oE~;0 zN^4MT1&0DW==?|9G!TV+4gdvQ#lY6!{d8w(WCP)!Kfabx=OG~v1aipBP9B(9fx93BRQ7t{%`^eOy^+w z{vL_Ff_N4?BmK|#AH*G-IpB~J)TEF0V;N+&T4dklGNPSTl%)mp;Z{)(R^jW>rF-rB zDhLib&5ykD^!6E=tganCf=7)DTfVj?7~f7F;iZx!^%w8{KKum70NwqSwe_+NH?M)a z=lXyEdeEjUoKbj3DEZ*h>&06lSC;Z+zb}~Y`h;T#A#TMTP8Tcz^k>4uuM2uk;%K=f z1BM%7hL0>_{EpD8mBTy%xh?(WK6OLO)Fg2?@CXlx`!A#x-k{d?KI&~8u3eK8A+9S> z13_Kwvsc(xm1t*|R&0|O&=-Sle4@n*ARry}Vw$53A~zy5dN+Q2!3rSW!wV2}@4XY% zCUWd0cB>*CA5sK~#8dDH*G?S}ewo@t(LjNAZ>l~?obK{s19CHZzNt;BvEWStKR(XHHux<-r=m0O%}Wg>4|#2#y|+O2Z= zql-SLzuI*O1bshO>yR}N;<6C~xeTeZ8D6igzm}iOomlU(9!Eq?I?y%K58XND~gX? zjaWdBd65;E_n|^2_m;+;75BELqJl70?b=w#;n2uWM%fcctHS76s>y(CMBZRFy;mtA z62hbX`c9#AM7M?{26*? z7X1}nZizaI9HcvSp#I{)`6JYyi zNq$=%)gT@`jCVrVHFqc{OUR(gxyWv$ijVa}&kE#&>8;21KbTzOZ=!C9>5sU|S>0W= zbhi)QaQpcDIq7GsJ|C+^kSB_Fcs-A8EOoK1%V(WRXXJ6)rs-{Zqjk zC!jXZlTcUXhhpiI$EKivI3SaFtuEZVd{Wr%J9GDuFR89g41{MAB1<|%XN&C(LEgj_ zM0|xmK*@r^sm{kx1i1|9w><)-F{&XfhxZQnyNPQcPX#z5SG&Lv4G?*Chmgz6$3a~H z)2*pk&l@IAuF(M=x!`O-g_SQoy~v7_z_=Z~H0yO^DdrIVm>$)n>e{U_IgQRp{|zuj z9tdxjy`&3ytu_HfwKSSdZxF->I9rem-y+v!}WogiV!7 z62L~>WoFBt7pl?_6cJ&zNYNI2fZ1%M?tp%()iJ_5aN8GjKl!Gvsp;{)s1-thg*~Uq zk?@G_3)M#W-Z;s7;UOhK-NIQU>g&EJmksX?Jp{V7Nt>8G5~aGr4Qj%9WWFptym5)=3V>9O5`dXgIz`Ldo4oEV5ZE6GH9^{Mw2` zw<8OU(knY&oce$>U5A^iF9PBQeK%}N`CXs1nvw{@f z{I{Ym?t(qFk3Ah`Sqq;L1b^59`LM|umi)v}4%S-bAU6R!My{-ju8ltPN6q)eA7HB^ zsyjr&trQoJWhS#ZiIh_~NkTdGp_Wnb&Or_D)j?~Hm?XpJ1^Q~6U0rMSD>}nM4(luy zD#IP6yrmAo){`~)L)Syt=72`OXDw?!>yT{0&ue(-Aedj2<3*tC3<%&sqS_}MhA>jX z5hP3j_ks}fwKrnh-omrFB}yfBNblv)7jLUvtT(=NtIXd12;%o;Tbmn0H;#x-k-c~g zpsDP=6UODkjv=M6?q-sUi{kxmG3*9&=pnek*=6s*w zGJZS{d2QdJFw?vK3mY%pyy}e2MnsWYFtpNst?h;FEx19UGitCmOl-@2cAzBdSPYb>0_i@4PJM6o8NKWeCmn9Z0x^T)RvV9xL$7@jl^ z4|_WnGJ4RfX5Y90#$p^ce>Qq46DKuPS8zmT0V}E5?AZRMVo+%LHN*Tp|DfgB{<>&= zesXW!%t~ONswnH!9?K4Z;k%h#jg0qQOa>>c!Ua!bg*Fx=dPcQ5VL%NFC zCITpUhQ#saP$vSFjTPRU@{RfG>g}zm$Kk^dF1HWE3R2D2LH6GUWCqtkQj}}hBH0I+ z+*2ix?^O#8Ewui6|80 zzC)>eC?Boox4AOnD@t75MJZ~KRliX9g2b*_HJ0~0fbtiB2k@bh+ft>=^V#h#xUMgu z2oujo{l4+pAyJ_9NX&2t&?e~}B@e~WoOxV?#;$={0R#v$^qL(sFpo9+bl{b?kq;(x znpf0(vi-IqT_GGufeU0}}1?E<-l;2R*V*sP)>4C(=px^ZK=L{Q3$)Ei11wF8x~--c;3b7H@V zASHoLcmaF^GGGcI5UlPOH7X_3LZZ6m^ikpp8hvr8@t9rgCr}Gvs9~9@6gxHusw<=} zqwGO={r+VT46|v@IF|$JAuOnX?cSW=@N<|>m12U4T$7p>U=x!(;pMS-2Ip3Iw_KP- zYt1Z7XC==+@Pk56MI6044hjV79a1k%sKzU=TQ05K3xZsaLm(J$#>rF~_XULeD)_#6 ziqF<0k}6d0&oyK(d+%wzkRLtU?-jJ&pMvpTIO<{LLOUDLrdjz^&|2jawXPRB+U?85 za6(P?+2E?P;PeVSPDx6Gk3QKfo zrnEb!!EvHav2uU)!d4pE8_H&Qe!X1Xdlctm)y`xGF|I)_$Bq3*%0S+=C)&VOM|iIA z#T-zr0>q*sZq8-#NoR%jDDQWfjmpEbJl3vao~)~g%5;7AOd|^k#J=dB)!|A zAo}W_kHQNzCLpd~U%fSGT~G=_IBx{jr3V#fJtFToITPR$5z2#5koeUVTFtxTXw(o? z24f+NLOjeXe13r6epGyRq`Djw72!*2DpaK$2k*&4ykuy0`K_o6? zQ-Jgd<5f~5g7o+&dtEx$@L5u`+9#NS(`Em9rl|8`9)%*5m@24Za@4BbanX|&DojV} zK7EVg?B_ zbo-d?i=ZQJJRU-4<&KaL^S8g7)~696U16C56;hL|-Fq~T7;joV9S|jcVd+$6zWGH5 zpw9&RkUO=8vrR2NO zR#vYvuS84@q@QNCc#{BwsyTQ9FB6MyKJ$Qr;IpN9qX4Qo^iLM0uUu>;W67iLijLSz zq0gyk&R`YNj1LmI9^X#E+3ASt2%D`;8r(n)SrtR<;K9_i7wV;ivKyxn{mR=kW>VeT z$pCY>wy~907#WtnmVHvhN2FJ3HDOLrDpw%bkmXuhvkXiq>=C6maC4y2Ag7?5Xu}x1 zyHBg{_^QzWAH=Kjtsf)T3+>VdKEtVvy#pXNQq?k%L=56f0OC4b;EpP*Cqp9C(*gdU z7fh#<$YdjM8*%BU))6|}khNfvr`zd!akFp@)Qwd8j9n(K>tanlLnOMncr|iTO~d38 zODH1HvZ#qX$Mnu#t#V7GwZvYI+dLRtdv>by3XF9P0W+YR1JJ^SPUXnq_PObR>;W-0 zr%oFr!9mv1Q)%nHn)OvyH)>`eD0Iqw6n_X5k6kYDP?%R|9A{O&Ek?Ruo*yNy_niNb!-wuQT-XR{cJu08Z*o~V0|H&<0KTcc z%Bfc%%xt`_E58Kd4@0N7*W?(-zLcvskQ0ltp)|RCK-WZs*Gy%E5_sm6L|~E zz=BI1X9mT`A{cFE(>APYnsO{EkvHd(yyhKzPkVffa0M~b>a&g@cbnpIpbn_! zK9KC3r1GId9Y=ZG9FOqp-C(mc(dNRFG<=;X*(7%u+Y$|CTMO{Sk^zG^r$E4IzHY#?*Y zwZv0G(2u2bNZV^&OHc6*Urt;pU9~3SW2I%2w)bok8w6H`7q(_7AD^ZhpYKp7prF~G zZbId|KFzo)Cek9wHZevuxJ=X#GZSoCS%wT4H*RDh>=BJNe0WeZOM6sxVjl!tu za@!mffmUqG_gvBA^_y+t%0ZDC5hgdE8>h{2%RXH#8Nw0R(NU2SC3FnhQczoYeJ>(w zldqLD$E;C-)cL)~Bws>aMxz9jiq=DCl+ZGK(&XjIBRm3f(2m3bk*t@t`yBO9sIP0| z;Uf`eg~+5|(Ky$lGx-_rMZQ`9cn&0;r|(+~q!G#h`&J69Z}&=;&r=27GkOebqTAn` zVj^5YI>-(l5ystF{OLDoW`pxnQPlGNwE&U4F+^$Wf(-Mp8q<398=o2w8XKuMtCvnd z4_P&@#hP6eiYjX(1x_>m-tdl1!0+m&hxV*+UzLJ3nB z*#q9%)j0ZYpg}4uvdN`(rO$~DmRH*6_=?Io;m%Gbncet)W3>4*=W}t552ZUm9oq-# z=xBb0L+8)t5k4S0`s*BW!3XhAcS7Dw%>Wlk_~ln9Ug5jBUTOA#%z51nk*@iQF4E^p zi4~dI4E$}1gfikhR~M#+HG#WVq2d+$=BEE163&%^arwwn^s_xjoX0c?G;Nur@}pU4 zK-jLY9BvfDE00#>oVj@E2kGLt4DEfx!1&+HTaTPNrm<f9|fa5C%dTq~1@E zw_OBi5gXJE+N)=z_KOnTbRCZ}85=?>3_U#2IX3{1<|n17cU=vy6zJ$1&|t@cMRX32 z2gJ-$d?uo_ht*{>VA@^#iACGvfcczgim9isO zvXE0iz&_c5oLRCpxvI>5N$bqZV_MIZ(-b5XuPdb~lsL57<=m9jwR)N^OO``+hs;}u zUU%iOh<66aNIM*4L)<@>bUlsV)Ebdq;(P>oF5YHLx?oVNb0AJgT^gi_vvX^7pR=8d zbNLBGhlQi5=$yRs*JLLHWh6NiCpE7KZ81BF-iQL*-zTN2{qPP);E{&aWIZJo_u@y5 z?JX4ImaBq+9WB`@p;Z_O+yA<}KB|oIXx0FGaC%qQ1KOpX|n~8pERvte#|< zw`lT~^j=+zI>*zgV-OECnm!e>^pJX+tV>D>Hc31^*9tP+l+PZAi&h!mh64y{OL~PY zS13vP2z`4EtV~&kPLt;$nXazF`43vUnns{7)(KVGpQO0>Dr~wtt6Gt_`Uxd(u##D? zt-_*wWAG~wN14ly;OtS#mcBl2%76EvbPh>De)^P9`$a1l2#9RyzRHMw%JLzn;$y9* zXZ^$oGHpNw2*}B0nbZ3_6;f1>_1}aNCY?!|f-3?DssjBJAoK4&N6@dMJ!k;QB2T%@tSpa zW@x-ix5GJRvUD=1&IihyD5wHAIh2y88HL}oCDMK|A5`GZb6R3^#mW{&3bfM{3Mh`A zfNtHB%ncOn2)?8b55*$3foJ{n*sU4A=6jTv4~PaodSB}`$T{I&l*lN8`O%13vZ3}fr9qB!NW~Qj4n!A2Vq$K)0Ga( zc!EHV(@1-wI`76Oi&w}|__GkF$8($AJtn1F{AA)5RC9Zn`ode{eD#Ja%xee=<>xYV zO4l6#sB$K_)W)p1y4ZBod+nV3M7w3eBwfT)K%Cj6JkcYl?HDTAJa#EMuGgR zW01N^!i7$hbx%EHmT!xAf0l8TCkjb6UqzIw@v+&jdU+`1`Sw=bbMtZs36h~>TwKh1MovQH^q27`S5l_@ z7!&LRORf9gv=1kOT+w`xQqu$o;X8}n$p>JST`i6iEQRv@jmUafbZ7=5O{KK2vCfr!OUDG47dqah$| zsKIPRHXECpruk)&ImHm^$iZF*qRwHrJ)aVbjtN^v4WL2!ev zdaSSeooa%Xf{yIA@&?>t^AgLcFM3q?V;iR%C>WBMYyA{o@oy!uY{Jc?qTgvavZ1W| zj=*#iQyX1d6Q0)yev=OoS|NU#6pY(_ET>=aGjrOqSp){nUi zUqb6yyf@tpV|ak>Ba#aqpr8M8Zx}?zpq!Rla8KIgqsC%$!j;4vwx2prxo)Pgyt$r| zJGd^P#bT<7NR%w`3tfHL?>fogTdFt7fcUb zU=i1B5(0!dyiF}ITTyvj@T}}Mu+;}M;C>)K{$@*6OdDHK{z@I>_{Rq)s||Iz?#{~T zqC4TXP&VY7b7{1Z6$c`E-&0HSCq<(nWajdZE}1e-_kmR5$lFS^srAauD+ZR)sxmoz z^a_X`4M_$ef^652NxL(EuR#aGS{h~Lr9~LB)?BG{uQ-bn9alc;sP6gCp!!OH-l_Y& zGpbjEhOM@@4sA)CmEW%r&s4WEYP7%)zsRnF*w@B z>XWipE?;-n0E6vmrsLjLdK+1#nrg(=P!zVhs2y>C(&25ws0LZFS+q_wRDtQP_z}jh zJ=xE^6pPNWcGdFwMqjs5NtjHAuuQI(t}#ca)y^x_L_@hey0y-sXnK z$Ti$Zk9w!D@s)n=(eW zRG!C=3*%5%lcWw7Bd!B>vDI^Brq@2~>GiC#$cu~x3>}ctcj++}2rOvkA|0gA4!saq zeGy{irAsWey0N@boQgayt&O=#EkaaUUiG})Jr$R(T2@|j zc7>6&r+&YlIAW?4DGz=u(>j$9z*OGGB#nY3aJHt_CXT zmAf!lb!t-#L!$h>lk3^B7;8aW2O`pjf*W5if*Q4Oxc-KBzasyEZ4}$?rC85cnQ?>> z8nNYByDISbwhlhoG+SZ4cf+&may<%48YucH8A}`j?v?w5J{ko|Xj%t4CSO6WErzp; z)x;MwnuMo)$HThQQLRpXP%E8Y=D^yecmohMdOI%j7MPV8SfmteO6g%@Jx-P2Yd z5lU7swN|2rK#53HNpiDz>--$Py( z$MjKYKdqyX*R>y~H^h<|6+tx$=9FK_;y1*QFQjjT3M)S>3)ym~nh-d!b_EHW-bt*WXhtC4r*4*kYA>OYZq37af%yxU2ieyZqw&UR zb--NYQ2mU2IPH4QT?|Eten=6bI)ppp2z=Nf4q@;mh84eAq8QA@wky*KIZ&iO6x7|U zKg9t$R&SVkFplV9f*vA)Qw7Wz!qKjwL2=E*+~l$;6hLx7EgNtQFww4cjU_tQJG8*M zTsw`52~X^Q^;x;997A+Z!cHSC43Q)je~;*C^`^v}Bo8JMo#CLsg{dLt%e546>2;Eb z@BoPh;al?Momzmo_En0|!N)f*)3&Y34KK|B#dvhmS24ufant;(uVR>Fk=K~$5wWc^ zBa;n8b&EkXr^tiFhbfSBTn+u-$nFRy`SiDOaIpqq`&iaMY<{`rK{V}XC>Ue}r|BXk zoVi3h?~XPo2xIT$#g(ZGfcR@A@$uXLo$9}5Mf>k#27g2E^Q1(kUIx( zg@kuOAtAGD^IptP)gvHP=Bb)-8B>qQBm{a=I_2=Tpe~=6MBs-kK>#IYAn8xwm+FT` zN@(C$@Fj^h_*@}1rpc#R5u{^A-d&>v5)dGH6m$SH4_`Ql>4qT&zZeMP>@ck?*WLrs zxJQbseDnjwA)syjC}xVMpz9)s;A9N0*0&Xe_PL}n578b255*Ggk;U8+zXpB6Gl=7wJR}^f+Yn6hUQ>S@cs$#>w5My5oX7qWWeNWxI)Y| z(7Lk>+UhV;h`B`wwncGO++jbsCk{0t ze?BQVhiL{1U8KOG4t@ovW?~DEA5nnG$M;|+hkE1! zJ3u!-rf)BR8FXhn^>1dttQ{|a8$5RCDE6cY!A8zK_dkkd3}{=|@SA9$dma#DSbr>} z!pv9D!fR&CJ$BfKS2jU4z3h|UJ!m+xwJ|vl^zc`9;EX)GAVpX0%j^m`S zfQD^;hn8r?2^@^P_c+tU5YUslUEIj0pjFVv^>}JRt&m*Y?Ed1L#Em{ndG!om@B#*j z8)-V~W_#@f*>G?C40_jMwoi;n_(Lz;HzmZr7i(uY#3L(keWlrt0Iyy*)yE51KXzwh zOwUho3ZJ0#7{+Mjj5zAzIO`+S__xi~cmof5;o-L~`UmcdV@#nxlLIl9?y6jXf=w?g z@Ca5INmrZ7x1!(fA+%z|EMB{M0)hKz#Y-_TY|G@%eta_ULaLP6il1l$D#i!gYVp*@ z$Z*Vh9q{hm2nT$^85P?35cnpWe0~tpcca|}s)%9iKQF={iLsa*M<=JSJWYGw0X%bW zKhg4CdFYW#&LqNfoO;H$j?aN&pVgT@(9V7>guN=ecBz@F52WzShkQXU&<50DmjQnt zssXb6xpnNkm5D=3$%3cML?{wS#U}C8#$7W00MNa=f1EauQwgGaSASP#60_qQgZeHLs!q8mZ=@Doa z3m7GC+^jorN;-w)HU#%l_8c$&e3|wpM;ypY^#dp0s{7DTYS1YwsB=tYrALF`c}lf< z`HirFf0FwcF_gZy9j z`oYN|keR#&3Q!*=I(Nytf~fpQuf^VmbqoUtRY*RBlmT@>1yXyol`#MUb#0Ru8fTsn zf7}cKpMgj!N)quJ1esmbFJkAbP~w-)eRua2eH|#g{U|-e)k19odd=dGK_R$=V|HYI z>l^s{sjoqWz`Gl;4pZ^J2F0tW!K>E-_S%3CI+R0266e#Y20WA2nnJ zguiVa4uRld&T9oL$X4C~+`g*yjr>V00|CbC$mqPqM!F_O;IW+sn#c5P!bQ-9L8m-l zwg=XN0Fv^qaQMUnMRo@sTd3apm@AnUnJ$?LnO0orkZ};r99XfNA|KLYlBRm>E=z)V zB@eQFyg3P#Dh%Gw>wNI0OT;_grZ^ZBB)R4rM1hvC);AZI zl?3#d8dgCy2BwP&2xDHni#G`2=YnA6y$Lb`jJy!y16?3qO9XiOsrEKP&j_;(ZRC>V zENS*#@%QzXh(pCH$)c`69;#x?ZjcDQ{GS@3x*R)zln&d$i!*>!};(w{(Che*umLmH?KImXg2xj0b4)EBfWK|$XNP-6DR^}UCMTjvd ze&v0SE94P&g$?_(nnZd+FhVFf5o`n&yiPdgYm@;$Pp&CN3pp@PDfjB{2@pE=a)Tn2 z+t6y?=QUB~wqRGkwVngoVxNm{t(joTVZXOFAwk(me_{F;(68c>rn=byN({iK3I?iw z9JFR8s>_7m!%7P>llKTd(NB-WhZk}P&bF4$VpwMO!=0s6GHB)K7chq34DtB{o0AMV5hj~JE_<9`JmqLWjbEqb zAO3tq*zNqvg}MzJsPb$Bzm!8&UME`LQNCOZ4i?78pc%2KKHc-=Vf=ATvg07Z^xg%O zb{WfOz4-ve>vQUX>Ku9<%aiEeU99mv6zm5iUw}yT7qMVlP;g+aj%i5B`WAyRd#AV{ z!>GzZ;80&M(<_GRixG#M*Iu`pw?c%#U4Kr|{;&O(M0j%D#rcYGy(4HyVa~|-FPw4m znk&tqYa|107Dv5n{BOBew@-Hi-2TGt^Y+CL=XS z*7akB8PJgzZa)Cp8nWIWFD8^^Rrscxo!;z|^yRv!?MHOU zY>mEI#XPdUUNJ0_$IRtWmwCMHr{~aTkK7s`gzdRtLF z8Y=B8jvW_U%Aa^0YM^KpP#N`wl9LCuJHN5D+~`=oj71P^Rv`Y|Cc2&%ino*(YQ&^H z=|Rmjv;ul}F<-)=EOD8;zvcFvnDJ`0D{5H19aO*!m=#>foB^K!>PEqhKsz_;&e%&_iWQrai0WO1z^Po51uTdH({E_;i+G`(br3GB5AMEs+dGG}E~t$jvY!+L@t|Q6;X_BDa)!>duXMFF7u_L)?D8(5hu{|GmV~>e_aaHTw-v}vQsjy-9kSbd=F<9J@Z5v)NtqOmD?Rw zyx>wBrrp)mQ4)O?Q{eSJ9 zWmr{PyZ04AK@37EC8R+CX{7|DTPbOkZfT`K5J?q@O-pw-5`u(CgM@U5NQ2Z8LC!rF zxc7d}d!FZd-w*F~opY{df7>|MT62s!#+c*2@BjZdjAE`o@YXs#oUeCF3s_ytpxMi? zEJ%5LSW;-J7aLs1LB{1q^* zbwOeKCWULf_2HY`^iJTPxqr63@2seg;z}4RVDBzL=C)9JRJ$0{~2*y42BNdx*08Rbg$T9>J3PcY;#E?c}VR){?)ID61$d)-w4XSjaYIX z1hq}ppv5-@|7^5+*{-{R(8&@uT6wvzJ@Z+?a*^jZEvfsM!P59YClfDpOxOyb1SSq6 zuHGb~!51Y<$rHoaT*MOy6Cutx&ETSpt%zx~FyoFSqx*G*_Y~=`oUnx?oLR9ipjWQ1 z5Ahxv^dEjf7EeVp?#%8TeO~Q&0x!f^&p#WcY=JoVkwAs@?Gi_1UOd5eAlJ^5AsPW> zSJ6O`U;mgG8En`8>0k?3WvknJh%glpEhmxoqxxqj(qQab0x-?gk{r|B0ey3rLZexZ zZ0M~D#cAz7Xwjv;rGUSl;0BSF#RJ$6k6!NN5t@@Z*oRtEm7yK8yqoT@u=1RNsG^Rqc5x#SeCmhP^n-`a0Q zw2o{%X`iJ=S4+7pQ&tyn(C_apeXpy8wCF6g&fk{Tfwm1$6!7^K2kSX+O~?@bg7H(~ z4EL~tRoZL*vESN4QrlyLynkAg5=!uo7C5^E`hrDLlhjSEp5<4bsG4(lezK~Bav6% z(wlSLLh~uFmjJ3z4omC6{-Yw;iI1d6zeZv{+f#;%d)(w%xpgdn*b~RKw(zLz(ogU+Nzy*0sjxAM6dt0OJFIcx2!wZ1c(u&QYgUmNCSEOct6iyQ*} zpTYj_mR;MM{EF$*T5vZArp{siV>7ZE@4JA6vJ?W4a*;JeZCwT)d#x7~g0V`=ed#Zj z>kfV)*uSB7UMH48=w9}(Eg#4%z5nM(s)ZK+>10$>nc!4}Xw!;ldhCsy^3s8T!Tyv4emr)_i3Zk(d#_QWV?@ z%X|Sev4v)!vbd4BiJ zTMWl1W-1A4HQh6#%7b}^Kj@^6_0UTkKVkazywho2OEg`>|8aa~5&k9zNmCj8h> z{k((P7>i#m{O$Pa4O)c=b0$K6VYCj7MCk7e?X;O`zUh^|?;PEaASA~;JiwA_)tY9s z62>Ub=@!TDR0f-Uw{!LBBrD*bV|DooQL{N{nprjPtrKI2K++A1eEd zhV3lM(W+sUAx`7~nwTy&U@(-UOFIIOah|@>45aA#Mpm6-RGt z2+qoqL-;;gRj*4p#kL|Ww(Sk+q43VNdA@x&KXS)MoX_~EeIZTh11U;kg=csv@A=%rGO z15yYQ#n^NhpeZwlVx!q><6}l%f;^Cq>W4&~YD;i70sTwgQx>7DKatXobkUpq@4p4! z`2tfP%|P1zv6{VnLYA(TbCS7~y><65%g2K^?Xl78$^)Iyr{xi86-1C*rh;73$)|U- zrTLCI0@GtVvTm=5TM^-Rv1PHveiD8NBvV@2x7D>Pq}^GBvtQRVwlUj zh1rMjE;?7QI(HjXsSclVy|HxI*agwqW%ie|CoSAsVQXaIlvypCB|wuNc2{POATe4TNaL$otB_#A9!Ul9l{> ziD{p;=j)SaZ+QG1 znHZhm8M=RhAL73;=YzB*nPZZwI$S%Rg5cVa^ZPKc@n$6JOx}8uE!%}~p4GQCGC$MZ z-H=Kg0xXDM{f71-#BrNPAfOM{n$5df!24MS2T?HsRis62j^4@g5R|m1_3X6lHETdfdDRmRTo^(Vu{ghPb6a`q=#4FIp{pd@H#a8OlStvMsxPRGHqBTm-5Ih{$<*TP`mv9Y})u(k?NP3Z5R+YS{(c?+e%?@Wi2MHpHLH3bUCY{Szu8 zr9xx}md!i$a5x2%ttzHY?gR~i1Vx)jihNYiq?>AIspp07$K4F$zKMfhPCK!~z$?`$ zI&bK$lGYNb>)a}64mk#bpeo&o&){MV;s2!dmt7-C@CAXF3}V;NfxP`ffRjS~PV*(} z8$CR(s7Ji7)5BVwA4wIMHY=D$YzcqLe*%@HgntGd-j)x6Mqvm5(TbBpvJd+k!8|UG@R0y`BI+1Ut`AGqL z*p>;;SBWJ6dOEpP*iJfHXD!r}7g-B^4Gf zt(kU~nD-O^ApkCA27>TkdJeK!6YwS-eG$z}5CYlR__snYQcb#&1)8xDtHw=eBmoJ2 z^b}&%iMyxV(;jcMMn>FG!K54!!61W|C3Y%X=FNOGDW?&a_CTq*I`8d_S@zWB$kRfW z-^LLs>VmAS2zG{uEUyZO0mgn z{o8ak=i+LJ*lzk?Jm-6)<>wel)X^>d2B0tEvfQNx%Gpwz7QN@gfkugJkE=-Q(=hey z87uMz%e=w+->j3+5<0NKD4SDaE2SlY?CP#Xg=eskodQRpNhocY_oOUUXfW0U8d7Co z#^w{y+1W!tvh2~V%fIgwc2@qt<0Di05KM?^_b2@vmr|Pts4V)k!eekgQPh0JOc?I} z-l-_`Bjth78X$Meki2GGsj@TLibB0B?l4sFU}aYHAg=L@o+g_WnxE0{^`)T-@6Uxv z1Qw=_AKBs5!058=x^fYR!xA4!p?&?(ONt}pMrqz3ust*H*96Y_v1LMQ8AQW*Mr3W? zuO%fxvbM#PW!W_Kl^PxkfcUqFzH zt2q5slF>m)yLho1?zI9>m*)%W%KHmhXTN8Sq$^UoOnRNNfZZ!s$5*CG*~~|M$FpTw zO2Y_WK}xxLgd~5lgji`%`o;pMP7Fi6`mSuC?hWcG^)5kI(<3?vM)_GC72)5!4`yNV zY%^pI;?Q9zKV;8ML4kL9`Gv<`GNH|`R_6UcQ2IGClST>kZAf?J116$Z=O^qPItx=c zx#}aEcRMEIe`^6Y7zZY0GYGu9Jb0FUL?wCIalUN1RNvgeb7eoFC(FHT*iGRFN~2*u zpni>BNoO8@nkN4p;EGPtS|vV8=QYA_?awQ&462gnQ{Bl3GTlv5rC0QSha}II!%aqO zBV5dGrc)FV#PYpnV|Kom06B7bUG>0(t?IJMOKb%0TOWI)eltoq2TF8q{+@B!v8d|& z5AX~%@(rVgujaMKICq+)tpe5AR)g;nu26r^M?$(QZ^59a{aTXZNs12mK3AT{LITz# z-g_1JQKhhH`5}=Yb3(I0iwx=LfyN=&wq$k4vnefFH{;CIE2&CGAa>U35b;5NPVD?)^}BU)vpxvzZ| z_Kh=RskZyht7`{jn+P&+GrKGT&s>e$T--6!q}8q`ARh(xFSK8CWHo0*y;Dz6fAhA% z=)Q)`WYUbMy~TU5*natyK6fu8RoitDF+3?^QhKw=&J!?4pypWxvXJBG-tPQVpK`2^OHS?+?h&-?dV!BJxnyFf@N?L9U3Ijioi+DvXRXZg_c7hn{mASDho6XQX@g!E(O+K)dRkD&gw_)+t&;^g zp&O+YOgXHVe5O!SA_r3h*k8KuKWoZdwuf+_(ol~N_wGXlan3%Owd(*`j>MB{_1?5* zDeoVThho(b`OPF6k+4ybL33*}!|7RKghi^}tFY`l zk(#E<$9&x;nr9?rF|UPOl;xKXuXr35R1in`=tQ`;ZtE-iG1#6=F7>;o{>c;1v!eup zh|!MqXEyxw{j%u7Pe1helI4uUP3L1U!|(Q9ZA%Q*F!9JNx@?x2S@HZe$4}F*)*oJ8 zSuqsRz@LrT-Y_GL-5#K@{cz>tM0rP3`M8Vr44GyJDn@;zb_DViqs~A9&7=He9(@e5 zuvbtG2aebX62rPu{|EMSwTe<-D-%biSQllo@iliobPi8Nchutw9 z4&M~{3;d_3VdUWcy7Sei9-35Ai~fY7gZb6-f}1a6u=+l~BCw3Ei~;5PTr`$Sk5_lJ z>FdYg1+OzgyF*D{XY^Xc7L70`zKV!u&V1FFg_(po!9A!wvmoh$lEF$anjz9OxxygO zBxdz{z1` zXX!e~A;og3->cJ1*8d^b?X=>nve1HK2o3!iX)R2cGLd`I1q+V}1YdHY3F!Vx9da@z zrc9|m>ZW(xaD@EZq2TDkTF<}_#_bi>o~1OSZd#6T)pDkK27Cs3a99<;m1t1nOhn-4 zpBWVha`~#OLX#G`HZoFkq2gpt7x}d)53^`Gvj6VlhZFzN&dwgBcaVx4`HET-C90 zo{kLDX9_M`AVLLuW(OD>_Ys&n!>h~vTF8b*6BcrY^9i8zqvxt zbGR-&szp^zqlx@v?jcEien}RGc3(7e-3!>D zed+pr&tlna=?pWE1W@;rm11%`du6Eo4f+@0Rd{Ju)J$c9$i(uC`(}l^i|D zqqGE*El)!|!qx5y>N|dVgH{xps&I8nADH3_XG~kuB8b6rRfZVA87gG3w4g$&djp5q z|GIw@?7w<(s93TAnoleVa_!h%HSaLBT+Pa|60s6U(phcGa=OX(Kf7`y@14 zn7E*de)-;mr|aNRV6A}B2vSw2`gCLOc-7&Le^yY`KA9}~xG#O8a1vzL+8YcY$!)yT z!%UEC+w+mAhUVK3QZS9vWG|St`E3W8Pk6*1VXhvB&H`=bXz)icn@OkEzEcfbzbh?H zyB1C8>TF^H6v6XnmtQFJ$bo5KKsN4uI_5PScq%DjKRBaRwPBN?W2C_HLgg*r(7obd z=SfBlIfx5Ado5A|@_H!h(HR9EGrc!PZ|$i{p#E4jpT?@3;S(tXS&Y~^nIvkY5_*oD z531^6`veVHe=FMEzUyf64A@1y_RCi{OiJukHEx&sDMkq=yRX##v?}*2WI_YqYeIXq zUFr5p^py5ZA~iaq-;Gbvf1)-S*F^c%PwR|rz2smw|P;{j&&f4VN(+AIYlyw{K|Ryrk6rx3txD2rR}8g9f59WYSV8&y+f8di1^Ut(A>$ z@xn}~5c{vZ6+@;HN?mG}iO;tS7EkHq?!B5+R2|sgpJtwxCfVxw%%jRwsd1`y zOcdF55@kZD+*GD1aJUzWMHA?Av)Zhj)k*$L=PzietVVq`H@4f>)lfzkQm2ao^V3EwA`xWP0X-tSd(2pmtF< zBm1Z7mBF+X-hKJ2=Mc;eFDGsNo0{R>!gR3aV?NZhrDUmBpS~LRB8&4uc;?M$>AMmJ z{!+OOFO_;Y#Fr1OtW?D-T`D(c>(;TsFYt1*kD}H@f)(m%PHe|>S&nH&5jIJT(9p(0 z1?R^z1FPYfyc*}M?3V7^)e>DwhW^0{PEYLn+-bQ>s_3vewE# z*WNqMWoQDmNBgnS_TSbElsQvQh-m1e*Y}BTFGs1sv5mrg>*CDAluYaCq<=Xtgt9{4 zWU?%$wwt#ixV0zb4nx6{**0f=_7S!INOK zNO!w*S66gB*%{-3p0w+Ji+W4Hn0J1}KXqSf%^?&8^K!-xGWU!0MUOa#32WrZl zI}Vmw?4}o0q91K$8V5br>>Hm(nDPj{FX@d(Cv>~nk8iPpWrR?nd>`!|a1!Wok^8gN z?C)hu$qs!7fm-n}|KxS^PQZM3*q%1paA64s&!F> z3PjS?iey&M50XPAdzJ3Ott9zj8gm2Wv`DP8bY4H2@i+Oynm13Hro3FX1P|S+}cQ`y^MI_ zA$5X@0+^uu%_7=&f8-FOVWB@R-@Qj3iHOf<)V9>}fey_Bgi7mW)!vti9$QV~I5D1G z>(enRqo<5Vosq0G#ZHWM(r0tFwjHf)6JJyOohN_Tuxv4>{1hOmC;nCN{yjB5_H0wR z6#M(ZuZUjOX0e{^P`!H03kA95b&WfO821+KHHsdD$ploO?`X-5^lY_1)O_P%KLaW4 zp@8)9`dOxE6ajL2bMS7lXbJ>ri4#MFMi! z`=b6dSCH1Gm`uwe7f^^Dk&8i!czE!|70{dOx)R=&m&CyX&Ctw%>^p<1BULaaQZ|xf zw4tSi);_x+@9K|S@RafJ7mi^ZopnKvNk9YL1)6on;~*}u8+FF{Lyj2HErFi z>s?Q5eDxAqd+9FMIvsMs`J}^NIF9}ML<>BrAm}a-ttVq*qn|z9eGZ)?t+Y3swuDO` zc7uQbT6;Tyz7~Cf$c6t;gQHf@wVwjOJRlSA%z27#@inj$)6SW2e9C!MVD3j; zm-Vk3dY$bSg@mdk$1yOm@fekaC$APT$xy#^nTl}{AZM0+v-xWvH-F`c;e7V9W5_jO zEL3aZW0d_4DpWfSygQG{EoBS)SZnGW7PDP+3xk#Z{YC%%`)KenFjwR~fapgCMc9Bv zxlb2c02!pAZJAv4Mth%J)dBs?ld}|N2&jZTk9oOYz(eg8W`F(U>@|uzp*@4>mrX$; zEl26PCSo}QCycPm-It4b+UK`s6s}`G5%@`%i1&VWyC6QUv6U+AaiNy+Ejl2Wd;; z9i9oV6XypezKsN2%|qpX;z(j6&8ZzXUWn%%rk@VP!GlH$<6>df67k5l?z6$*|LN5H z7Q_dAkor%k1wC2`zztIMLvE&>wkotM1ww7QENdmLJZS9aL1~rOpnVzg@x=QECsIMzve{0I-|~NxvfWkqsXJ-TJCGI%7IPYqPKc5`bOor#--*ies&q^uEAv z0hk*%)i#hx#6tol!+xWKSAAj!V328W5DL3^E- zPuU5{zp!l(Izp*FoQ!&^nG`31e$`h?{$-MtH^7x^G<<(mz}fL91Rdg&Kk)WLLEKCOuDNY0=YE zBUo%xD7rxe?8 z-3=+;UL1z*%r608o7xqttjx45Z>>)k>);kUlwA+xI6K1BK-XE)M5LuM#f4!K#10hW zbT~Zc#&XThxG9_wBM-l%qw0a#4EI&BI|g!8HQ&jMTMd&4(hJVDOtFVYkpfaK+aZ=X ze2W==noVC*%vw|}w$l^`&_YuHc3PUX3PuYkXKX5y0c4L%XXgD5uea`vM*J==iJ?(w z%%$L{jW1P$dSe^514h@7)tst(6$3-B&S_{*lL^N3h;3( zc`bw9Kfgi7Ck6!Bpl|5|(x~P#H4Ers=AlZHgQ2$+y$rgO17Z{qs^4<;h!LhjDv{H& zV(MA{+$jn`YRhl^gn!8&#&UtdlhsYc@8oe2HN9GhOVUDW9D8TEMEr5^ed0uBZ#ADn zY}LX`<#29f8bGlUhmX5J6(XyE>^M8d1dPyoaT6}jtnW_-j#WTLvHW!W0|I@q+!`&u zQ$p{{!6~DX$lDw|1M?@_aqMCF^)oQjewBcpD*d{@PRpZMpgoZ3l=n;w2?li!<2nO} zSe9GDXUJnwqv6#>9c0w$(p+XBL@#I|xY(}bb;soD>MaasyN7+E>$;K>0HJa)i>twRvVQetv8>< zpOc&ne0dXO6$g5gfqd~0P%qFFz9r>1!RLU9w1I!J7Te}Q5tJcR6a4R!@F(IE_<2Oh zZ!(E40;I0{TSHJ+Q>Xsu@GeYK?p+IM2FbPH+8gljk#*Y@p1+VR4gLrEZmu_Hx0~c! z0(Fu=+Qu^mb^HEq&{U}aF9cVwMvM8>$y$(v(g=bzr@z>;Fqw$oem!S=&C7aHOGuIk zH9gQZBocT)ZLk9UKM%4ES>W`zl`am96b1;BJ;-yCHU8ubo*Ol8J^LQ4aN z&g<{bFx9e+Pm|tiDYgaWr|89((+N?}=Wz4kpxcFkZfw^Y&<8o89tzd`sDoLi${4ij z0rCz>5)^e~4lFOG>2Z!(3jw>-8O{cSdGYOcrLsNlTaxVO?>jyR^rhe81 zi$fFWb^MsuS7@{yX9Csrk66(sOX^RX@r}d1>i8yB7;;*%X~PoN2ZGDHSb1JvmN3|i zr>HB$umcM!fcmd`oY5-#GU|+8+&ki~_f1YMxOeHndSsSUA#M*erCi6lhv{s(e-Io{ zmaA`LOh8s%&CLn3Y)CpNI%yl_x)j?1luT9l&UNZXO(FNIFVn}*2abKH=;ySm+{=DP z%x?;+Y?rMu_Hsl>gd9FF4;7`Xc=MnHc$3yY=vr&iPRhe>m?D1bvTpHUG&-7d?d&LB zaAd=#x1P`EGJ*HJLk>%;$lQiEmfdl${K_arJd#-q`4hwUvAD_EtW<-vnDryCg*=`y ztX84L82^AV0mU4hWrHUCu92nIE zPFgP6Tmgeov2=takxQ4T57!m{%R^p{5R{w%%?%)bs=W5(zv~PgGd!goE96WvM;H^7A~!nW)wkXUqfQO0qGFkA;W>E&*t!bHZjB2J zzjfdqCnJMq#|XPj*nu2Mt?POS<66^l(jAxA`Ud;Klf+%P%{wJiEOGK*m$cEVn(&)= z&v)uwts0oWRK3!UTNX_JT6w&#*8W(6MN}@1M)v|iy)lf$JS)YZx#6@w9tM~NrAY=| zEaz{I;htH3bK(W|9d@fdJM8zAg)_t+FGggNpN->}`!`#LtPk{fNsbo%fNhLQ{i6aJ z$NA~XI_yGxU42Q;@!rq;(?9Bl=#~WE5B+o#cum6Q>+d|{*z4tRc011Dm(V>A8mg8y zM~Ol9dv3|utP`+Y`A1Yhgu8ZF`xWHIbmpT|(c<~uE`aRo(gYDW>Dkg&Y>!PASD(Z&eR@3Yj@S_EImLpSbkF^N~~@0bKS zN@Gmr>|yJ#LhHA<$h@u{wVp%zsR7|(lXCG-9=09}?iY(kTpH7s?WNs|^xK9wVdE3M zi&wK_vaX=p`~7I8NSqo>>(VVH66(ir^@+iah4&8zpP zmb06ogEBYz0;+t^caejACYVLo=#FD+qdglc4>1(nZfhO;co*a zGKv52VS1`H?hRX>qwhz5r6vv4T4|c<>BDf~=);^a)1o4a4`1nrS9g=a-?(${LSE>v zuZ)IOt)n_t{^;h=yPg49zH&{&MgQT$a>A45UuP};_EGE-3HY0Q_63>0f7l4Ri<8Y5 uy-obx3;*syq{F-a-GxXm{0|s~sPpZ0_s(fEiTWOc|D?qg#PUS%d;bULHm#`u literal 0 HcmV?d00001 From 8d500354c82fc7abc72bc6dd0e010d8ec98d1bfb Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 02:54:08 +0300 Subject: [PATCH 08/12] chore: add config.example for the new schema --- config.example.toml | 129 ++++++++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 34 deletions(-) diff --git a/config.example.toml b/config.example.toml index 15f0fff..cd5d760 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,11 +1,14 @@ -# Path to where aliases in .toml will be stored. If omitted, no aliases setting/displaying would work. -aliases = "/home/monitoring/config/cosmos-transactions-bot-aliases.toml" -# Timezone in which time (like undelegation finish time) will be displayed. Defaults to "Etc/GMT", so UTC+0 +# Path to where aliases in .toml will be stored. +# If omitted, no aliases setting/displaying would work. +aliases = "cosmos-transactions-bot-aliases.toml" +# Timezone in which time (like undelegation finish time) will be displayed. +# Defaults to "Etc/GMT", so UTC+0 timezone = "Europe/Moscow" # Logging configuration [log] -# Log level. Set to "debug" or "trace" to make it more verbose, or to "warn"/"error" to make it less verbose. +# Log level. Set to "debug" or "trace" to make it more verbose, or to "warn"/"error" +# to make it less verbose. # Defaults to "info" level = "info" # If true, all logs would be displayed in JSON. Useful if you are using centralized logging @@ -14,47 +17,42 @@ json = false # Reporters configuration. [[reporters]] +# Reporter name. Should be unique. +name = "telegram-1" # Reporter type. Currently, the only supported type is "telegram", which is the default. type = "telegram" # Telegram config configuration. Required if the type is "telegram". # See README.md for more details. +# Has 3 params: +# - token - bot token +# - chat - a chat/channel to post messages to +# - admins - a whitelist of user IDs allowed to send commands to the bot, optional but recommended. telegram-config = { token = "xxx:yyy", chat = 12345, admins = [67890] } -# Per-chain configuration. There can be multiple chains. -[[chains]] -# Chain codename, required. +# There can be multiple reporters. +[[reporters]] +name = "telegram-2" +type = "telegram" +telegram-config = { token = "zzz:aaa", chat = 98765, admins = [43210] } + +# Subscriptions config. See README.md on how this schema works. +[[subscriptions]] +# Reporter name to send events matching this subscription to. +# Should be one of the names of the reporters declared above, or the app won't start +# with the config validation error +reporter = "telegram-1" +# Subscription name, for metrics. Should be unique. +name = "subscription-1" + +# Chain subscriptions for this subscription. +[[subscriptions.chains]] +# Chain name. Should be one of the names declared below in chains section, +# or the app won't start with the config validation error. name = "cosmos" -# Chain pretty name, optional. If provided, would be used in reports, if not, -# codename would be used. -pretty-name = "Cosmos Hub" -# Tendermint RPC nodes to subscribe to. At least one is required, better to have multiple ones -# as a failover. -tendermint-nodes = [ - "https://rpc.cosmos.quokkastake.io:443", -] -# API nodes to get blockchain data (validators, proposals etc.) from. -api-nodes = [ - "https://api.cosmos.quokkastake.io", -] -# Queries, see README.md for details. -queries = [ - "tx.height > 0" -] # Filter, see README.md for details. filters = [ "message.action = '/cosmos.gov.v1beta1.MsgVote'", ] -# Denoms list. -denoms = [ - # Each denom inside must have "denom" and "display-denom" fields and additionaly - # denom-coefficient (set to 1000000 by default) and coingecko-currency. - # Example: if there's a transfer transaction for 10,000,000 uatom, - # and the coingecko price for $ATOM is 10$ and if all fields are set, - # instead of displaying amount as `10000000.000000uatom` it would be displayed - # as `10.000000atom ($100.00)`. - # If coingecko-currency is omitted, no price would be displayed. - { denom = "uatom", display-denom = "atom", denom-coefficient = 1000000, coingecko-currency = "cosmos" } -] # If set to true and there is a message not supported by this app, # it would post a message about that, otherwise it would ignore such a message. # Defaults to false. @@ -84,6 +82,58 @@ filter-internal-messages = true # - `Error: RPC error -32000 - Server error: subscription was cancelled (reason: Tendermint exited)` # If this is set to true (default), such messages would be displayed, if not, they will be skipped. log-node-errors = true + +# There can be multiple chain subscriptions per subscription. +[[subscriptions.chains]] +name = "sentinel" +filters = ["message.action = '/cosmos.staking.v1beta1.MsgDelegate'"] + +# There can also be multiple subscriptions. This one, for example, +# sends everything to a different reporter. +[[subscriptions]] +name = "subscription-2" +reporter = "telegram-2" +[[subscriptions.chains]] +name = "cosmos" +filters = ["message.action = '/cosmos.staking.v1beta1.MsgUndelegate'"] +[[subscriptions.chains]] +name = "sentinel" +filters = ["message.action = '/cosmos.staking.v1beta1.MsgBeginRedelegate'"] + + + +# Per-chain configuration. +[[chains]] +# Chain codename, required. +name = "cosmos" +# Chain pretty name, optional. If provided, would be used in reports, if not, +# codename would be used. +pretty-name = "Cosmos Hub" +# Tendermint RPC nodes to subscribe to. At least one is required, better to have multiple ones +# as a failover. +tendermint-nodes = [ + "https://rpc.cosmos.quokkastake.io:443", +] +# API nodes to get blockchain data (validators, proposals etc.) from. +api-nodes = [ + "https://api.cosmos.quokkastake.io", +] +# Queries, see README.md for details. +# Defaults to ["tx.height > 0"], so basically all transactions on chain. +queries = [ + "tx.height > 0" +] +# Denoms list. +denoms = [ + # Each denom inside must have "denom" and "display-denom" fields and additionaly + # denom-coefficient (set to 1000000 by default) and coingecko-currency. + # Example: if there's a transfer transaction for 10,000,000 uatom, + # and the coingecko price for $ATOM is 10$ and if all fields are set, + # instead of displaying amount as `10000000.000000uatom` it would be displayed + # as `10.000000atom ($100.00)`. + # If coingecko-currency is omitted, no price would be displayed. + { denom = "uatom", display-denom = "atom", denom-coefficient = 1000000, coingecko-currency = "cosmos" } +] # Explorer configuration. # Priorities: # 1) ping.pub @@ -111,3 +161,14 @@ block-link-pattern = "https://mintscan.io/cosmos/blocks/%s" # A pattern for validator links for the explorer. validator-link-pattern = "https://mintscan.io/cosmos/validators/%s" + +# There can be multiple chains. +[[chains]] +name = "sentinel" +pretty-name = "Sentinel" +tendermint-nodes = ["https://rpc.sentinel.quokkastake.io:443"] +api-nodes = ["https://api.sentinel.quokkastake.io"] +denoms = [ + { denom = "udvpn", display-denom = "dvpn", coingecko-currency = "sentinel" } +] +mintscan-prefix = "sentinel" From f71ffe93a01058657a6faf0d0872e3e3ca8d04c9 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 02:58:22 +0300 Subject: [PATCH 09/12] chore: fixed linting --- pkg/config/toml_config/chain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/toml_config/chain.go b/pkg/config/toml_config/chain.go index cde8053..b1628e7 100644 --- a/pkg/config/toml_config/chain.go +++ b/pkg/config/toml_config/chain.go @@ -15,7 +15,7 @@ type Chain struct { Queries []string `default:"[\"tx.height > 1\"]" toml:"queries"` MintscanPrefix string `toml:"mintscan-prefix"` PingPrefix string `toml:"ping-prefix"` - PingBaseUrl string `default:"https://ping.pub" toml:"ping-base-url"` + PingBaseUrl string `default:"https://ping.pub" toml:"ping-base-url"` Explorer *Explorer `toml:"explorer"` Denoms DenomInfos `toml:"denoms"` } From acfdd4356bbbb91e6044a3d86c2c59296cd73e95 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 17:12:32 +0300 Subject: [PATCH 10/12] feat: refactor metrics, part 1 --- pkg/app.go | 14 ++- pkg/constants/constants.go | 10 ++- pkg/filterer/filterer.go | 32 +++++-- pkg/metrics/metrics.go | 133 ++++++++++++++++------------- pkg/reporters/reporter.go | 1 - pkg/reporters/telegram/telegram.go | 9 +- pkg/types/report.go | 1 + 7 files changed, 121 insertions(+), 79 deletions(-) diff --git a/pkg/app.go b/pkg/app.go index 02f3cfb..07b7a1e 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -77,13 +77,11 @@ func (a *App) Start() { for _, reporter := range a.Reporters { reporter.Init() - a.MetricsManager.LogReporterEnabled(reporter.Name(), reporter.Enabled()) - if reporter.Enabled() { - a.Logger.Info(). - Str("name", reporter.Name()). - Str("type", reporter.Type()). - Msg("Init reporter") - } + a.MetricsManager.LogReporterEnabled(reporter.Name(), reporter.Type()) + a.Logger.Info(). + Str("name", reporter.Name()). + Str("type", reporter.Type()). + Msg("Init reporter") } a.NodesManager.Listen() @@ -115,7 +113,7 @@ func (a *App) Start() { Str("hash", report.Reportable.GetHash()). Msg("Got report") - a.MetricsManager.LogReport(report) + a.MetricsManager.LogReport(report, reporterName) rawReport.Reportable.GetAdditionalData(fetcher) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 6d85dc4..2bcf1f5 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -1,9 +1,17 @@ package constants +type EventFilterReason string + const ( - PrometheusMetricsPrefix = "cosmos_transactions_bot_" + PrometheusMetricsPrefix string = "cosmos_transactions_bot_" ReporterTypeTelegram string = "telegram" + + EventFilterReasonTxErrorNotLogged EventFilterReason = "tx_error_not_logged" + EventFilterReasonNodeErrorNotLogged EventFilterReason = "node_error_not_logged" + EventFilterReasonUnsupportedMsgTypeNotLogged EventFilterReason = "unsupported_msg_type_not_logged" + EventFilterReasonFailedTxNotLogged EventFilterReason = "failed_tx_not_logged" + EventFilterReasonEmptyTxNotLogged EventFilterReason = "empty_tx_not_logged" ) func GetReporterTypes() []string { diff --git a/pkg/filterer/filterer.go b/pkg/filterer/filterer.go index e3d71c4..5115abc 100644 --- a/pkg/filterer/filterer.go +++ b/pkg/filterer/filterer.go @@ -4,6 +4,7 @@ import ( "fmt" configPkg "main/pkg/config" configTypes "main/pkg/config/types" + "main/pkg/constants" messagesPkg "main/pkg/messages" metricsPkg "main/pkg/metrics" "main/pkg/types" @@ -65,6 +66,7 @@ func (f *Filterer) GetReportableForReporters( Chain: report.Chain, Node: report.Node, Reportable: reportableFiltered, + Subscription: subscription, ChainSubscription: chainSubscription, } } @@ -83,7 +85,11 @@ func (f *Filterer) FilterForChainAndSubscription( // Filtering out TxError only if chain's log-node-errors = true. if _, ok := reportable.(*types.TxError); ok { if !chainSubscription.LogNodeErrors { - f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent( + chainSubscription.Chain, + reportable.Type(), + constants.EventFilterReasonTxErrorNotLogged, + ) f.Logger.Debug().Msg("Got transaction error, skipping as node errors logging is disabled") return nil } @@ -94,7 +100,11 @@ func (f *Filterer) FilterForChainAndSubscription( if _, ok := reportable.(*types.NodeConnectError); ok { if !chainSubscription.LogNodeErrors { - f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent( + chainSubscription.Chain, + reportable.Type(), + constants.EventFilterReasonNodeErrorNotLogged, + ) f.Logger.Debug().Msg("Got node error, skipping as node errors logging is disabled") return nil } @@ -106,7 +116,11 @@ func (f *Filterer) FilterForChainAndSubscription( tx, ok := reportable.(*types.Tx) if !ok { f.Logger.Error().Str("type", reportable.Type()).Msg("Unsupported reportable type, ignoring.") - f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent( + chainSubscription.Chain, + reportable.Type(), + constants.EventFilterReasonUnsupportedMsgTypeNotLogged, + ) return nil } @@ -114,7 +128,11 @@ func (f *Filterer) FilterForChainAndSubscription( f.Logger.Debug(). Str("hash", tx.GetHash()). Msg("Transaction is failed, skipping") - f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent( + chainSubscription.Chain, + reportable.Type(), + constants.EventFilterReasonFailedTxNotLogged, + ) return nil } @@ -151,7 +169,11 @@ func (f *Filterer) FilterForChainAndSubscription( f.Logger.Debug(). Str("hash", tx.GetHash()). Msg("All messages in transaction were filtered out, skipping.") - f.MetricsManager.LogFilteredEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogFilteredEvent( + chainSubscription.Chain, + reportable.Type(), + constants.EventFilterReasonEmptyTxNotLogged, + ) return nil } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index b4e58f6..c479c46 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -21,33 +21,38 @@ type Manager struct { logger zerolog.Logger config configPkg.MetricsConfig - lastBlockHeightCollector *prometheus.GaugeVec - lastBlockTimeCollector *prometheus.GaugeVec - nodeConnectedCollector *prometheus.GaugeVec - reconnectsCounter *prometheus.CounterVec - + // Chains metrics + lastBlockHeightCollector *prometheus.GaugeVec + lastBlockTimeCollector *prometheus.GaugeVec + chainInfoGauge *prometheus.GaugeVec successfulQueriesCollector *prometheus.CounterVec failedQueriesCollector *prometheus.CounterVec + eventsTotalCounter *prometheus.CounterVec + eventsFilteredCounter *prometheus.CounterVec - eventsTotalCounter *prometheus.CounterVec - eventsFilteredCounter *prometheus.CounterVec - eventsMatchedCounter *prometheus.CounterVec + // Node metrics + nodeConnectedCollector *prometheus.GaugeVec + reconnectsCounter *prometheus.CounterVec + // Reporters metrics reportsCounter *prometheus.CounterVec reportEntriesCounter *prometheus.CounterVec + reporterEnabledGauge *prometheus.GaugeVec - reporterEnabledGauge *prometheus.GaugeVec - reporterQueriesCounter *prometheus.CounterVec + // Subscriptions metrics + eventsMatchedCounter *prometheus.CounterVec + // App metrics appVersionGauge *prometheus.GaugeVec startTimeGauge *prometheus.GaugeVec - chainInfoGauge *prometheus.GaugeVec } func NewManager(logger *zerolog.Logger, config configPkg.MetricsConfig) *Manager { return &Manager{ logger: logger.With().Str("component", "metrics").Logger(), config: config, + + // Chain metrics lastBlockHeightCollector: promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: constants.PrometheusMetricsPrefix + "last_height", Help: "Height of the last block processed", @@ -56,10 +61,10 @@ func NewManager(logger *zerolog.Logger, config configPkg.MetricsConfig) *Manager Name: constants.PrometheusMetricsPrefix + "last_time", Help: "Time of the last block processed", }, []string{"chain"}), - nodeConnectedCollector: promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: constants.PrometheusMetricsPrefix + "node_connected", - Help: "Whether the node is successfully connected (1 if yes, 0 if no)", - }, []string{"chain", "node"}), + chainInfoGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: constants.PrometheusMetricsPrefix + "chain_info", + Help: "Chain info, with constant 1 as value and pretty_name and chain as labels", + }, []string{"chain", "pretty_name"}), successfulQueriesCollector: promauto.NewCounterVec(prometheus.CounterOpts{ Name: constants.PrometheusMetricsPrefix + "node_successful_queries_total", Help: "Counter of successful node queries", @@ -68,22 +73,46 @@ func NewManager(logger *zerolog.Logger, config configPkg.MetricsConfig) *Manager Name: constants.PrometheusMetricsPrefix + "node_failed_queries_total", Help: "Counter of failed node queries", }, []string{"chain", "node", "type"}), + eventsTotalCounter: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: constants.PrometheusMetricsPrefix + "events_total", + Help: "WebSocket events received by node", + }, []string{"chain", "node"}), + eventsFilteredCounter: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: constants.PrometheusMetricsPrefix + "events_filtered", + Help: "WebSocket events filtered out by chain, type and reason", + }, []string{"chain", "type", "reason"}), + + // Node metrics + nodeConnectedCollector: promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: constants.PrometheusMetricsPrefix + "node_connected", + Help: "Whether the node is successfully connected (1 if yes, 0 if no)", + }, []string{"chain", "node"}), + reconnectsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: constants.PrometheusMetricsPrefix + "reconnects_total", + Help: "Node reconnects count", + }, []string{"chain", "node"}), + + // Reporter metrics + reporterEnabledGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: constants.PrometheusMetricsPrefix + "reporter_enabled", + Help: "Whether the reporter is enabled (1 if yes, 0 if no)", + }, []string{"name", "type"}), reportsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ Name: constants.PrometheusMetricsPrefix + "node_reports", Help: "Counter of reports send", - }, []string{"chain"}), + }, []string{"chain", "reporter", "type", "subscription"}), reportEntriesCounter: promauto.NewCounterVec(prometheus.CounterOpts{ Name: constants.PrometheusMetricsPrefix + "node_report_entries_total", Help: "Counter of report entries send", + }, []string{"chain", "reporter", "type", "subscription"}), + + // Subscription metrics + eventsMatchedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: constants.PrometheusMetricsPrefix + "events_matched", + Help: "WebSocket events matching filters by chain", }, []string{"chain", "type"}), - reporterEnabledGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: constants.PrometheusMetricsPrefix + "reporter_enabled", - Help: "Whether the reporter is enabled (1 if yes, 0 if no)", - }, []string{"name"}), - reporterQueriesCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "reporter_queries", - Help: "Reporters' queries count ", - }, []string{"chain", "name", "query"}), + + // App metrics appVersionGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: constants.PrometheusMetricsPrefix + "version", Help: "App version", @@ -92,26 +121,6 @@ func NewManager(logger *zerolog.Logger, config configPkg.MetricsConfig) *Manager Name: constants.PrometheusMetricsPrefix + "start_time", Help: "Unix timestamp on when the app was started. Useful for annotations.", }, []string{}), - eventsTotalCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "events_total", - Help: "WebSocket events received by node", - }, []string{"chain", "node"}), - eventsFilteredCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "events_filtered", - Help: "WebSocket events filtered out by chain", - }, []string{"chain", "type"}), - eventsMatchedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "events_matched", - Help: "WebSocket events matching filters by chain", - }, []string{"chain", "type"}), - reconnectsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "reconnects_total", - Help: "Node reconnects count", - }, []string{"chain", "node"}), - chainInfoGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: constants.PrometheusMetricsPrefix + "chain_info", - Help: "Chain info, with constant 1 as value and pretty_name and chain as labels", - }, []string{"chain", "pretty_name"}), } } @@ -125,10 +134,6 @@ func (m *Manager) SetAllDefaultMetrics(chains []*configTypes.Chain) { } } func (m *Manager) SetDefaultMetrics(chain *configTypes.Chain) { - m.reportsCounter. - With(prometheus.Labels{"chain": chain.Name}). - Add(0) - m.chainInfoGauge. With(prometheus.Labels{"chain": chain.Name, "pretty_name": chain.PrettyName}). Set(1) @@ -217,25 +222,35 @@ func (m *Manager) LogTendermintQuery(chain string, query queryInfo.QueryInfo, qu } } -func (m *Manager) LogReport(report types.Report) { +func (m *Manager) LogReport(report types.Report, reporterName string) { m.reportsCounter. - With(prometheus.Labels{"chain": report.Chain.Name}). + With(prometheus.Labels{ + "chain": report.Chain.Name, + "reporter": reporterName, + "type": report.Reportable.Type(), + "subscription": report.Subscription.Name, + }). Inc() for _, entry := range report.Reportable.GetMessages() { m.reportEntriesCounter. With(prometheus.Labels{ - "chain": report.Chain.Name, - "type": entry.Type(), + "chain": report.Chain.Name, + "reporter": reporterName, + "type": entry.Type(), + "subscription": report.Subscription.Name, }). Inc() } } -func (m *Manager) LogReporterEnabled(name string, enabled bool) { +func (m *Manager) LogReporterEnabled(name, reporterType string) { m.reporterEnabledGauge. - With(prometheus.Labels{"name": name}). - Set(utils.BoolToFloat64(enabled)) + With(prometheus.Labels{ + "name": name, + "type": reporterType, + }). + Set(1) } func (m *Manager) LogAppVersion(version string) { @@ -250,9 +265,13 @@ func (m *Manager) LogWSEvent(chain string, node string) { Inc() } -func (m *Manager) LogFilteredEvent(chain string, eventType string) { +func (m *Manager) LogFilteredEvent(chain string, eventType string, reason constants.EventFilterReason) { m.eventsFilteredCounter. - With(prometheus.Labels{"chain": chain, "type": eventType}). + With(prometheus.Labels{ + "chain": chain, + "type": eventType, + "reason": string(reason), + }). Inc() } diff --git a/pkg/reporters/reporter.go b/pkg/reporters/reporter.go index 4dccc8e..1cbdb2c 100644 --- a/pkg/reporters/reporter.go +++ b/pkg/reporters/reporter.go @@ -16,7 +16,6 @@ type Reporter interface { Init() Name() string Type() string - Enabled() bool Send(report types.Report) error } diff --git a/pkg/reporters/telegram/telegram.go b/pkg/reporters/telegram/telegram.go index a908c5c..2931d7b 100644 --- a/pkg/reporters/telegram/telegram.go +++ b/pkg/reporters/telegram/telegram.go @@ -96,11 +96,6 @@ func (reporter *Reporter) Init() { reporter.TelegramBot = bot go reporter.TelegramBot.Start() } - -func (reporter *Reporter) Enabled() bool { - return reporter.Token != "" && reporter.Chat != 0 -} - func (reporter *Reporter) GetTemplate(name string) (*template.Template, error) { if cachedTemplate, ok := reporter.Templates[name]; ok { reporter.Logger.Trace().Str("type", name).Msg("Using cached template") @@ -127,14 +122,14 @@ func (reporter *Reporter) GetTemplate(name string) (*template.Template, error) { } func (reporter *Reporter) Render(templateName string, data interface{}) (string, error) { - template, err := reporter.GetTemplate(templateName) + reportTemplate, err := reporter.GetTemplate(templateName) if err != nil { reporter.Logger.Error().Err(err).Str("type", templateName).Msg("Error loading template") return "", err } var buffer bytes.Buffer - err = template.Execute(&buffer, data) + err = reportTemplate.Execute(&buffer, data) if err != nil { reporter.Logger.Error().Err(err).Str("type", templateName).Msg("Error rendering template") return "", err diff --git a/pkg/types/report.go b/pkg/types/report.go index 4a7b975..6862ea3 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -6,6 +6,7 @@ import ( type Report struct { Chain types.Chain + Subscription *types.Subscription ChainSubscription *types.ChainSubscription Node string Reportable Reportable From ef590a71db43956a5f2bde1ab320a97a39fd0516 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 17:16:23 +0300 Subject: [PATCH 11/12] feat: refactor metrics, part 2 --- pkg/filterer/filterer.go | 6 +++--- pkg/metrics/metrics.go | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/filterer/filterer.go b/pkg/filterer/filterer.go index 5115abc..d5ff6cf 100644 --- a/pkg/filterer/filterer.go +++ b/pkg/filterer/filterer.go @@ -94,7 +94,7 @@ func (f *Filterer) FilterForChainAndSubscription( return nil } - f.MetricsManager.LogMatchedEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogMatchedEvent(chainSubscription.Chain, reportable.Type(), subscription.Name) return reportable } @@ -109,7 +109,7 @@ func (f *Filterer) FilterForChainAndSubscription( return nil } - f.MetricsManager.LogMatchedEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogMatchedEvent(chainSubscription.Chain, reportable.Type(), subscription.Name) return reportable } @@ -178,7 +178,7 @@ func (f *Filterer) FilterForChainAndSubscription( } tx.Messages = messages - f.MetricsManager.LogMatchedEvent(subscription.Name, reportable.Type()) + f.MetricsManager.LogMatchedEvent(chainSubscription.Chain, reportable.Type(), subscription.Name) return tx } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index c479c46..8aeb8ae 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -110,7 +110,7 @@ func NewManager(logger *zerolog.Logger, config configPkg.MetricsConfig) *Manager eventsMatchedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ Name: constants.PrometheusMetricsPrefix + "events_matched", Help: "WebSocket events matching filters by chain", - }, []string{"chain", "type"}), + }, []string{"chain", "type", "subscription"}), // App metrics appVersionGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{ @@ -275,9 +275,13 @@ func (m *Manager) LogFilteredEvent(chain string, eventType string, reason consta Inc() } -func (m *Manager) LogMatchedEvent(chain string, eventType string) { +func (m *Manager) LogMatchedEvent(chain string, eventType string, subscription string) { m.eventsMatchedCounter. - With(prometheus.Labels{"chain": chain, "type": eventType}). + With(prometheus.Labels{ + "chain": chain, + "type": eventType, + "subscription": subscription, + }). Inc() } From 8b485b7ff472b5e7851a756cd7b49ab6835b409b Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Feb 2024 17:26:57 +0300 Subject: [PATCH 12/12] feat: refactor metrics, part 3 --- pkg/app.go | 5 +++-- pkg/metrics/metrics.go | 37 +++++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pkg/app.go b/pkg/app.go index 07b7a1e..2a19b3e 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -113,8 +113,6 @@ func (a *App) Start() { Str("hash", report.Reportable.GetHash()). Msg("Got report") - a.MetricsManager.LogReport(report, reporterName) - rawReport.Reportable.GetAdditionalData(fetcher) reporter := a.Reporters.FindByName(reporterName) @@ -123,6 +121,9 @@ func (a *App) Start() { a.Logger.Error(). Err(err). Msg("Error sending report") + a.MetricsManager.LogReport(report, reporterName, false) + } else { + a.MetricsManager.LogReport(report, reporterName, true) } } case <-quit: diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 8aeb8ae..4c66002 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -35,9 +35,10 @@ type Manager struct { reconnectsCounter *prometheus.CounterVec // Reporters metrics - reportsCounter *prometheus.CounterVec - reportEntriesCounter *prometheus.CounterVec - reporterEnabledGauge *prometheus.GaugeVec + reporterReportsCounter *prometheus.CounterVec + reporterErrorsCounter *prometheus.CounterVec + reportEntriesCounter *prometheus.CounterVec + reporterEnabledGauge *prometheus.GaugeVec // Subscriptions metrics eventsMatchedCounter *prometheus.CounterVec @@ -97,13 +98,17 @@ func NewManager(logger *zerolog.Logger, config configPkg.MetricsConfig) *Manager Name: constants.PrometheusMetricsPrefix + "reporter_enabled", Help: "Whether the reporter is enabled (1 if yes, 0 if no)", }, []string{"name", "type"}), - reportsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "node_reports", - Help: "Counter of reports send", + reporterReportsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: constants.PrometheusMetricsPrefix + "reporter_reports", + Help: "Counter of reports sent successfully", + }, []string{"chain", "reporter", "type", "subscription"}), + reporterErrorsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: constants.PrometheusMetricsPrefix + "reporter_errors", + Help: "Counter of failed reports sends", }, []string{"chain", "reporter", "type", "subscription"}), reportEntriesCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Name: constants.PrometheusMetricsPrefix + "node_report_entries_total", - Help: "Counter of report entries send", + Name: constants.PrometheusMetricsPrefix + "report_entries_total", + Help: "Counter of messages types per each successfully sent report", }, []string{"chain", "reporter", "type", "subscription"}), // Subscription metrics @@ -222,8 +227,20 @@ func (m *Manager) LogTendermintQuery(chain string, query queryInfo.QueryInfo, qu } } -func (m *Manager) LogReport(report types.Report, reporterName string) { - m.reportsCounter. +func (m *Manager) LogReport(report types.Report, reporterName string, success bool) { + if !success { + m.reporterErrorsCounter. + With(prometheus.Labels{ + "chain": report.Chain.Name, + "reporter": reporterName, + "type": report.Reportable.Type(), + "subscription": report.Subscription.Name, + }). + Inc() + return + } + + m.reporterReportsCounter. With(prometheus.Labels{ "chain": report.Chain.Name, "reporter": reporterName,