From 3952b7738ae2e30c08ea55786a5f3d129399fc69 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 14 Sep 2021 23:34:08 -0400 Subject: [PATCH] add account activity and rewards metrics --- helium-blockchain-exporter.go | 150 ++++++++++++++++++++++------------ heliumapi/accounts.go | 69 ++++++++++++++++ 2 files changed, 169 insertions(+), 50 deletions(-) diff --git a/helium-blockchain-exporter.go b/helium-blockchain-exporter.go index 4056ef3..5bd8081 100644 --- a/helium-blockchain-exporter.go +++ b/helium-blockchain-exporter.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "strings" + "time" "github.com/helium-blockchain-exporter/heliumapi" "github.com/prometheus/client_golang/prometheus" @@ -20,7 +21,20 @@ type metricInfo struct { // Exporter collect metrics from the helium blockchain api and exports them as prometheus metrics. type Exporter struct { - Accounts []string + Accounts []Account +} + +// Account represents a helium account +type Account struct { + hotspots []Hotspot + hash string + lastRewardsUpdate time.Time +} + +// Hotspot represent a helium hotspot +type Hotspot struct { + hash string + lastRewardsUpdate time.Time } const ( @@ -47,43 +61,43 @@ var ( // helium stats metrics statsValidators = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "validators"), - "The number of validators.", + prometheus.BuildFQName(namespace, "stats", "validators_total"), + "The total number of validators.", nil, nil, ), - prometheus.GaugeValue, + prometheus.CounterValue, } statsOuis = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "ouis"), - "The number of organization unique identifiers.", + prometheus.BuildFQName(namespace, "stats", "ouis_total"), + "The total number of organization unique identifiers.", nil, nil, ), - prometheus.GaugeValue, + prometheus.CounterValue, } statsHotspotsDataOnly = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "hotspots_dataonly"), - "The number of data only hotspots.", + prometheus.BuildFQName(namespace, "stats", "hotspots_dataonly_total"), + "The total number of data only hotspots.", nil, nil, ), - prometheus.GaugeValue, + prometheus.CounterValue, } statsBlocks = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "blocks"), - "The height/number of blocks in the blockchain.", + prometheus.BuildFQName(namespace, "stats", "blocks_total"), + "The total height/number of blocks in the blockchain.", nil, nil, ), - prometheus.GaugeValue, + prometheus.CounterValue, } statsChallenges = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "challenges"), - "The number of challenges.", + prometheus.BuildFQName(namespace, "stats", "challenges_total"), + "The total number of challenges.", nil, nil, ), - prometheus.GaugeValue, + prometheus.CounterValue, } statsCities = metricInfo{ prometheus.NewDesc( @@ -95,8 +109,8 @@ var ( } statsConsensusGroups = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "consensus_groups"), - "The number of consensus groups.", + prometheus.BuildFQName(namespace, "stats", "consensus_groups_total"), + "The total number of consensus groups.", nil, nil, ), prometheus.GaugeValue, @@ -111,11 +125,11 @@ var ( } statsHotspots = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "stats", "hotspots"), - "The number of hotspots.", + prometheus.BuildFQName(namespace, "stats", "hotspots_total"), + "The total number of hotspots.", nil, nil, ), - prometheus.GaugeValue, + prometheus.CounterValue, } statsTokenSupply = metricInfo{ prometheus.NewDesc( @@ -145,28 +159,28 @@ var ( } accountRewardsHnt = metricInfo{ prometheus.NewDesc( - prometheus.BuildFQName(namespace, "account", "reward_hnt"), + prometheus.BuildFQName(namespace, "account", "rewards_hnt"), "The number of HNT token rewarded to this account.", commonAccountLabels, nil, ), prometheus.GaugeValue, } - accountDepositsHnt = metricInfo{ - prometheus.NewDesc( - prometheus.BuildFQName(namespace, "account", "deposits_hnt_total"), - "The number of HNT token deposited to this account.", - commonAccountLabels, nil, - ), - prometheus.CounterValue, - } - accountWithdrawalsHnt = metricInfo{ - prometheus.NewDesc( - prometheus.BuildFQName(namespace, "account", "withdrawals_hnt_total"), - "The number of HNT token withdrawn from this account.", - commonAccountLabels, nil, - ), - prometheus.CounterValue, - } + // accountDepositsHnt = metricInfo{ + // prometheus.NewDesc( + // prometheus.BuildFQName(namespace, "account", "deposits_hnt_total"), + // "The number of HNT token deposited to this account.", + // commonAccountLabels, nil, + // ), + // prometheus.CounterValue, + // } + // accountWithdrawalsHnt = metricInfo{ + // prometheus.NewDesc( + // prometheus.BuildFQName(namespace, "account", "withdrawals_hnt_total"), + // "The number of HNT token withdrawn from this account.", + // commonAccountLabels, nil, + // ), + // prometheus.CounterValue, + // } // helium hotspot metrics ) @@ -190,8 +204,8 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- accountBalanceHnt.Desc ch <- accountActivity.Desc ch <- accountRewardsHnt.Desc - ch <- accountDepositsHnt.Desc - ch <- accountWithdrawalsHnt.Desc + // ch <- accountDepositsHnt.Desc + // ch <- accountWithdrawalsHnt.Desc } // Collect fetches the data from the helium blockchain api and delivers them as Prometheus metrics. @@ -200,7 +214,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.collectOracleMetrics(ch) e.collectStatsMetrics(ch) for _, account := range e.Accounts { - e.collectAccountMetrics(ch, account) + e.collectAccountMetrics(ch, &account) } } @@ -258,8 +272,20 @@ func (e *Exporter) collectStatsMetrics(ch chan<- prometheus.Metric) { } // collectStatsMetrics collect metrics in the account group from the helium api -func (e *Exporter) collectAccountMetrics(ch chan<- prometheus.Metric, account string) { - accountForAddress, err := heliumapi.GetAccountForAddress(account) +func (e *Exporter) collectAccountMetrics(ch chan<- prometheus.Metric, account *Account) { + accountForAddress, err := heliumapi.GetAccountForAddress(account.hash) + if err != nil { + fmt.Println(err) + return + } + + accountActivityForAddress, err := heliumapi.GetActivityCountsForAccount(account.hash) + if err != nil { + fmt.Println(err) + return + } + + accountRewardTotalsForAddress, err := heliumapi.GetRewardTotalsForAccount(account.hash, &account.lastRewardsUpdate, nil) if err != nil { fmt.Println(err) return @@ -267,17 +293,44 @@ func (e *Exporter) collectAccountMetrics(ch chan<- prometheus.Metric, account st ch <- prometheus.MustNewConstMetric( accountBalanceHnt.Desc, accountBalanceHnt.Type, float64(accountForAddress.Data.Balance), - account, + account.hash, ) + for accType, count := range accountActivityForAddress.Data { + ch <- prometheus.MustNewConstMetric( + accountActivity.Desc, accountActivity.Type, float64(count), + account.hash, accType, + ) + } + ch <- prometheus.MustNewConstMetric( + accountRewardsHnt.Desc, accountRewardsHnt.Type, accountRewardTotalsForAddress.Data.Sum, + account.hash, + ) + account.lastRewardsUpdate, err = time.Parse(time.RFC3339, accountRewardTotalsForAddress.Meta.MaxTime) + if err != nil { + fmt.Printf("failed to parse time \"%v\", value of %v will be bad: %v\n", accountRewardTotalsForAddress.Meta.MaxTime, accountRewardsHnt.Desc.String(), err) + } } // NewExporter returns an initialized Exporter -func NewExporter(accounts []string) (*Exporter, error) { +func NewExporter(accountHashs []string) (*Exporter, error) { + accounts := make([]Account, 0) + for _, accountHash := range accountHashs { + if accountHash != "" { + accounts = append(accounts, NewAccount(accountHash)) + } + } return &Exporter{ Accounts: accounts, }, nil } +func NewAccount(hash string) Account { + return Account{ + hash: hash, + lastRewardsUpdate: time.Now(), + } +} + func main() { fHeliumAccounts := flag.String("accounts", "", "A comma-delimited list of helium accounts to scrape.") fMetricsPath := flag.String("metricpath", "/metrics", "The metrics path") @@ -285,10 +338,7 @@ func main() { fListenPort := flag.String("listenPort", "9111", "The http server listen port") flag.Parse() - heliumAccounts := make([]string, 0) - if *fHeliumAccounts != "" { - heliumAccounts = strings.Split(*fHeliumAccounts, ",") - } + heliumAccounts := strings.Split(*fHeliumAccounts, ",") serverAddr := *fListenAddress + ":" + *fListenPort e, err := NewExporter(heliumAccounts) @@ -311,6 +361,6 @@ func main() { }) http.Handle(*fMetricsPath, promhttp.HandlerFor(r, promhttp.HandlerOpts{})) - fmt.Printf("exporter listening on %v\n", serverAddr) + fmt.Printf("listening on %v\n", serverAddr) http.ListenAndServe(serverAddr, nil) } diff --git a/heliumapi/accounts.go b/heliumapi/accounts.go index d2487f3..0a87426 100644 --- a/heliumapi/accounts.go +++ b/heliumapi/accounts.go @@ -3,6 +3,7 @@ package heliumapi import ( "encoding/json" "fmt" + "time" ) type Account struct { @@ -18,6 +19,27 @@ type Account struct { } `json:"data"` } +type ActivityCounts struct { + Data map[string]int +} + +type RewardTotal struct { + Meta struct { + MinTime string `json:"min_time"` + MaxTime string `json:"max_time"` + } `json:"meta"` + + Data struct { + Total float64 `json:"total"` + Sum float64 `json:"sum"` + Stddev float64 `json:"stddev"` + Min float64 `json:"min"` + Median float64 `json:"median"` + Max float64 `json:"max"` + Avg float64 `json:"avg"` + } `json:"data"` +} + func GetAccountForAddress(account string) (*Account, error) { path := "/v1/accounts/" + account @@ -36,3 +58,50 @@ func GetAccountForAddress(account string) (*Account, error) { return &respobject, nil } + +func GetActivityCountsForAccount(account string) (*ActivityCounts, error) { + path := "/v1/accounts/" + account + "/activity/count" + + // query the api + respBody, err := getHeliumApi(path, nil) + if err != nil { + return nil, err + } + + // unmarshal the response + respobject := ActivityCounts{} + err = json.Unmarshal(respBody, &respobject) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response from path %v: %v", path, err) + } + + return &respobject, nil + +} + +func GetRewardTotalsForAccount(account string, minTime *time.Time, maxTime *time.Time) (*RewardTotal, error) { + path := "/v1/accounts/" + account + "/rewards/sum" + params := map[string]string{} + if minTime != nil { + params["min_time"] = minTime.UTC().Format("2006-01-02T15:04:05Z") + } + if maxTime != nil { + params["max_time"] = minTime.UTC().Format("2006-01-02T15:04:05Z") + } + + // query the api + respBody, err := getHeliumApi(path, ¶ms) + if err != nil { + return nil, err + } + + // unmarshal the response + respobject := RewardTotal{} + err = json.Unmarshal(respBody, &respobject) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response from path %v: %v", path, err) + } + + return &respobject, nil + +}