diff --git a/exporter.go b/exporter.go index df017f5..8e25d1b 100644 --- a/exporter.go +++ b/exporter.go @@ -3,6 +3,7 @@ package main import ( "flag" "log" + "math" "net/http" "os" "strconv" @@ -221,6 +222,14 @@ var ( ), prometheus.GaugeValue, } + hotspotSynced = metricInfo{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, "hotspot", "synced"), + "Whether a hotspot is synced with the blockchain.", + commonHotspotLabels, nil, + ), + prometheus.GaugeValue, + } hotspotRelayed = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "relayed"), @@ -245,6 +254,22 @@ var ( ), prometheus.GaugeValue, } + hotspot5dWitnesses = metricInfo{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, "hotspot", "5d_witnesses"), + "The number of hotspots that witnessed this hotspot in the last 5 days.", + commonHotspotLabels, nil, + ), + prometheus.GaugeValue, + } + hotspot5dWitnessed = metricInfo{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, "hotspot", "5d_witnessed"), + "The number of hotspots this hotspot witnessed in the last 5 days.", + commonHotspotLabels, nil, + ), + prometheus.GaugeValue, + } hotspotGeocodeInfo = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "geocode_info"), @@ -309,9 +334,12 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- accountWithdrawalsHnt.Desc ch <- hotspotUp.Desc + ch <- hotspotSynced.Desc ch <- hotspotRelayed.Desc ch <- hotspotBlocks.Desc ch <- hotspotRewardsScale.Desc + ch <- hotspot5dWitnesses.Desc + ch <- hotspot5dWitnessed.Desc ch <- hotspotGeocodeInfo.Desc ch <- hotspotAntennaInfo.Desc ch <- hotspotActivity.Desc @@ -581,7 +609,10 @@ func (e *Exporter) collectHotspotMetrics(wg *sync.WaitGroup, ch chan<- prometheu for _, hotspotData := range *hotspotsForAddress { // collect hotspot metric requiring extra queries in a new routine - wg.Add(2) + wg.Add(5) + go e.collectHotspotSyncedMetrics(wg, ch, account, hotspotData) + go e.collectHotspotWitnessesMetrics(wg, ch, account, hotspotData) + go e.collectHotspotWitnessedMetrics(wg, ch, account, hotspotData) go e.collectHotspotActivityMetrics(wg, ch, account, hotspotData) go e.collectHotspotRewardsMetrics(wg, ch, account, hotspotData) @@ -612,8 +643,68 @@ func (e *Exporter) collectHotspotMetrics(wg *sync.WaitGroup, ch chan<- prometheu } } +// collectSyncedMetrics calculate if the hotspot is in sync with the blockchain +func (e *Exporter) collectHotspotSyncedMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.Hotspot) { + defer wg.Done() + + isSynced := 0. + if hotspotData.Status.Online == "online" && hotspotData.Status.Timestamp != "" { + statusTime, err := time.Parse(time.RFC3339Nano, hotspotData.Status.Timestamp) + if err != nil { + log.Println(err) + return + } + heightAtUpdate, err := heliumapi.GetHeight(&statusTime) + if err != nil { + log.Println(err) + return + } + // if the block height and the height reported by the hotspot is within 10 blocks, we consider it in sync + if math.Abs(float64(hotspotData.Status.Height-heightAtUpdate)) <= 10 { + isSynced = 1. + } + } + + ch <- prometheus.MustNewConstMetric( + hotspotSynced.Desc, hotspotActivity.Type, isSynced, + account.Address, hotspotData.Address, hotspotData.Name, + ) +} + +// collectHotspotWitnessesMetrics collect the total number witnesses of a hotspot in the last 5d +func (e *Exporter) collectHotspotWitnessesMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.Hotspot) { + defer wg.Done() + + hotspotWitnesses, err := heliumapi.GetWitnessesForHotspot(hotspotData.Address) + if err != nil { + log.Println(err) + return + } + + ch <- prometheus.MustNewConstMetric( + hotspot5dWitnesses.Desc, hotspotActivity.Type, float64(len(*hotspotWitnesses)), + account.Address, hotspotData.Address, hotspotData.Name, + ) +} + +// collectHotspotWitnessedMetrics collect the total number hotspots witnessed in the last 5d +func (e *Exporter) collectHotspotWitnessedMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.Hotspot) { + defer wg.Done() + + hotspotWitnessed, err := heliumapi.GetWitnessedForHotspot(hotspotData.Address) + if err != nil { + log.Println(err) + return + } + + ch <- prometheus.MustNewConstMetric( + hotspot5dWitnessed.Desc, hotspotActivity.Type, float64(len(*hotspotWitnessed)), + account.Address, hotspotData.Address, hotspotData.Name, + ) +} + // collectHotspotActivityMetrics collect the total number of activities executed by a hotspot from the helium api -func (e *Exporter) collectHotspotActivityMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.AccountHotspot) { +func (e *Exporter) collectHotspotActivityMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.Hotspot) { defer wg.Done() hotspotActivityForAddress, err := heliumapi.GetHotspotActivityCounts(hotspotData.Address) @@ -631,7 +722,7 @@ func (e *Exporter) collectHotspotActivityMetrics(wg *sync.WaitGroup, ch chan<- p } // collectHotspotRewardsMetrics collect the total rewards accumulated by a hotspot from the helium api -func (e *Exporter) collectHotspotRewardsMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.AccountHotspot) { +func (e *Exporter) collectHotspotRewardsMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.Hotspot) { defer wg.Done() hotspotRewardTotalsForAddress, err := heliumapi.GetRewardsTotalForHotspot(hotspotData.Address, &e.StartTime, nil) diff --git a/heliumapi/accounts.go b/heliumapi/accounts.go index ed4eab9..a4a1436 100644 --- a/heliumapi/accounts.go +++ b/heliumapi/accounts.go @@ -110,7 +110,7 @@ func GetRewardTotalsForAccount(account string, minTime *time.Time, maxTime *time } // query https://docs.helium.com/api/blockchain/accounts#hotspots-for-account -func GetHotspotsForAccount(account string) (*[]AccountHotspot, error) { +func GetHotspotsForAccount(account string) (*[]Hotspot, error) { path := "/v1/accounts/" + account + "/hotspots" // query the api diff --git a/heliumapi/block.go b/heliumapi/block.go new file mode 100644 index 0000000..227b66d --- /dev/null +++ b/heliumapi/block.go @@ -0,0 +1,31 @@ +package heliumapi + +import ( + "encoding/json" + "fmt" + "time" +) + +// query https://docs.helium.com/api/blockchain/blocks/#height +func GetHeight(maxTime *time.Time) (int, error) { + path := "/v1/blocks/height" + params := map[string]string{} + if maxTime != nil { + params["max_time"] = maxTime.UTC().Format("2006-01-02T15:04:05Z") + } + + // query the api + respBody, err := getHeliumApi(path, ¶ms) + if err != nil { + return -1, err + } + + // unmarshal the response + respobject := HeightResp{} + err = json.Unmarshal(respBody, &respobject) + if err != nil { + return -1, fmt.Errorf("failed to unmarshal response from %v: %v", path, err) + } + + return respobject.Data.Height, nil +} diff --git a/heliumapi/hotspots.go b/heliumapi/hotspots.go index dba606e..ab7b688 100644 --- a/heliumapi/hotspots.go +++ b/heliumapi/hotspots.go @@ -52,3 +52,43 @@ func GetRewardsTotalForHotspot(hotspot string, minTime *time.Time, maxTime *time return &respobject.Data, nil } + +// query https://docs.helium.com/api/blockchain/hotspots#witnesses-for-a-hotspot +func GetWitnessesForHotspot(hotspot string) (*[]Hotspot, error) { + path := "/v1/hotspots/" + hotspot + "/witnesses" + + // query the api + respBody, err := getHeliumApi(path, nil) + if err != nil { + return nil, err + } + + // unmarshal the response + respobject := WitnessesResp{} + err = json.Unmarshal(respBody, &respobject) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response from %v: %v", path, err) + } + + return &respobject.Data, nil +} + +// query https://docs.helium.com/api/blockchain/hotspots#witnessed-for-a-hotspot +func GetWitnessedForHotspot(hotspot string) (*[]Hotspot, error) { + path := "/v1/hotspots/" + hotspot + "/witnessed" + + // query the api + respBody, err := getHeliumApi(path, nil) + if err != nil { + return nil, err + } + + // unmarshal the response + respobject := WitnessedResp{} + err = json.Unmarshal(respBody, &respobject) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response from %v: %v", path, err) + } + + return &respobject.Data, nil +} diff --git a/heliumapi/types.go b/heliumapi/types.go index b799681..23bc054 100644 --- a/heliumapi/types.go +++ b/heliumapi/types.go @@ -4,6 +4,46 @@ type AccountResp struct { Data Account `json:"data"` } +type AccountHotspotsResp struct { + Data []Hotspot `json:"data"` +} + +type ActivityCounts map[string]int +type ActivityCountsResp struct { + Data ActivityCounts +} + +type RewardTotalResp struct { + Meta struct { + MinTime string `json:"min_time"` + MaxTime string `json:"max_time"` + } `json:"meta"` + + Data RewardTotal `json:"data"` +} + +type CurrentOraclePriceResp struct { + Data CurrentOraclePrice `json:"data"` +} + +type BlockchainStatsResp struct { + Data BlockchainStats `json:"data"` +} + +type WitnessesResp struct { + Data []Hotspot `json:"data"` +} + +type HeightResp struct { + Data struct { + Height int `json:"height"` + } `json:"data"` +} + +type WitnessedResp struct { + Data []Hotspot `json:"data"` +} + type Account struct { Address string `json:"address"` Balance int `json:"balance"` @@ -15,11 +55,7 @@ type Account struct { SpeculativeNonce int `json:"speculative_nonce"` } -type AccountHotspotsResp struct { - Data []AccountHotspot `json:"data"` -} - -type AccountHotspot struct { +type Hotspot struct { Lng float64 `json:"lng"` Lat float64 `json:"lat"` TimestampAdded string `json:"timestamp_added"` @@ -57,21 +93,6 @@ type AccountHotspot struct { Address string `json:"address"` } -type ActivityCountsResp struct { - Data ActivityCounts -} - -type ActivityCounts map[string]int - -type RewardTotalResp struct { - Meta struct { - MinTime string `json:"min_time"` - MaxTime string `json:"max_time"` - } `json:"meta"` - - Data RewardTotal `json:"data"` -} - type RewardTotal struct { Total float64 `json:"total"` Sum float64 `json:"sum"` @@ -82,19 +103,11 @@ type RewardTotal struct { Avg float64 `json:"avg"` } -type CurrentOraclePriceResp struct { - Data CurrentOraclePrice `json:"data"` -} - type CurrentOraclePrice struct { Price int `json:"price"` Block int `json:"block"` } -type BlockchainStatsResp struct { - Data BlockchainStats `json:"data"` -} - type BlockchainStats struct { BlockTime struct { LastDay struct {