package main import ( "flag" "log" "net/http" "os" "strconv" "strings" "sync" "time" "github.com/helium-blockchain-exporter/heliumapi" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/time/rate" ) // metricInfo is a metric exported by the helium blockchain exporter type metricInfo struct { Desc *prometheus.Desc Type prometheus.ValueType } // Exporter collect metrics from the helium blockchain api and exports them as prometheus metrics. type Exporter struct { Accounts []Account StartTime time.Time } // NewExporter returns an initialized Exporter func NewExporter(accountAddress []string) (*Exporter, error) { accounts := make([]Account, 0) for _, accountAddress := range accountAddress { if accountAddress != "" { accounts = append(accounts, NewAccount(accountAddress)) } } return &Exporter{ Accounts: accounts, StartTime: time.Now(), }, nil } // Account represents a helium account type Account struct { Address string LastUpdate time.Time RewardsTotal map[string]int Tx AccountTx Hotspots map[string]Hotspot UpdateLock *sync.Mutex } func NewAccount(address string) Account { return Account{ Address: address, LastUpdate: time.Now(), RewardsTotal: map[string]int{ "data_credits": 0, "poc_challengers": 0, "poc_challengees": 0, "poc_witnesses": 0, }, Tx: AccountTx{ DepositTotal: 0, WithdrawalTotal: 0, }, Hotspots: map[string]Hotspot{}, UpdateLock: &sync.Mutex{}, } } type AccountTx struct { DepositTotal int WithdrawalTotal int } // Hotspot represents a hotspot type Hotspot struct { Address string LastUpdate time.Time RewardsTotal map[string]int UpdateLock *sync.Mutex } func NewHotspot(address string) Hotspot { return Hotspot{ Address: address, LastUpdate: time.Now(), RewardsTotal: map[string]int{ "data_credits": 0, "poc_challengers": 0, "poc_challengees": 0, "poc_witnesses": 0, }, UpdateLock: &sync.Mutex{}, } } const ( namespace = "helium" blockchain_oracle_factor = 100000000 blockchain_hnt_factor = 100000000 ) var ( // labels commonAccountLabels = []string{"account"} commonHotspotLabels = append(commonAccountLabels, "hotspot", "hotspot_name") // exporter metrics // helium oracle metrics oraclePrice = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "oracle", "price_usd"), "The oracle price of an HNT token in USD.", nil, nil, ), prometheus.GaugeValue, } // helium stats metrics statsValidators = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "validators"), "The total number of validators.", nil, nil, ), prometheus.GaugeValue, } statsOuis = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "ouis"), "The total number of organization unique identifiers.", nil, nil, ), prometheus.GaugeValue, } statsHotspotsDataOnly = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "hotspots_dataonly"), "The total number of data only hotspots.", nil, nil, ), prometheus.GaugeValue, } statsBlocks = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "blocks"), "The height of the blockchain.", nil, nil, ), prometheus.GaugeValue, } statsChallenges = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "challenges"), "The total number of challenges.", nil, nil, ), prometheus.GaugeValue, } statsCities = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "cities"), "The number of cities with at least one helium hotspot.", nil, nil, ), prometheus.GaugeValue, } statsConsensusGroups = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "consensus_groups"), "The total number of consensus groups.", nil, nil, ), prometheus.GaugeValue, } statsCountries = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "countries"), "The number of countries with at least on helium hotspot.", nil, nil, ), prometheus.GaugeValue, } statsHotspots = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "hotspots"), "The total number of hotspots.", nil, nil, ), prometheus.GaugeValue, } statsTokenSupply = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "token"), "The total supply of HNT tokens in circulation.", nil, nil, ), prometheus.GaugeValue, } // helium account metrics accountBalanceHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "balance_hnt"), "The number of HNT token owned by an account.", commonAccountLabels, nil, ), prometheus.GaugeValue, } accountActivity = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "activity_total"), "The total number of time an activity occurred in an account.", append(commonAccountLabels, "type"), nil, ), prometheus.CounterValue, } accountRewardsHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "rewards_hnt_total"), "The number of HNT token rewarded to an account.", append(commonAccountLabels, "type"), nil, ), prometheus.CounterValue, } accountDepositsHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "deposits_hnt_total"), "The number of HNT tokens deposited to an account.", commonAccountLabels, nil, ), prometheus.CounterValue, } accountWithdrawalsHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "withdrawals_hnt_total"), "The number of HNT tokens withdrawn from an account.", commonAccountLabels, nil, ), prometheus.CounterValue, } // helium hotspot metrics hotspotUp = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "up"), "Whether a hotspot is online.", commonHotspotLabels, nil, ), prometheus.GaugeValue, } hotspotRelayed = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "relayed"), "Whether a hotspot is relayed.", commonHotspotLabels, nil, ), prometheus.GaugeValue, } hotspotBlocks = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "blocks"), "The block height of a hotspot. Check on the hotspot itself for the most recent data.", commonHotspotLabels, nil, ), prometheus.GaugeValue, } hotspotBlocksDelta = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "blocks_delta"), "The gap between the height of a hotspot and the height of the blockchain. A large negative gap may indicate the hotspot is out of sync. Check on the hotspot itself for the most recent data.", commonHotspotLabels, nil, ), prometheus.GaugeValue, } hotspotStatusTimestamp = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "status_timestamp"), "The the last time a hotspot gossiped its status on the blockchain.", commonHotspotLabels, nil, ), prometheus.GaugeValue, } hotspotRewardsScale = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "rewards_scale"), "The reward scale of a hotspot.", commonHotspotLabels, nil, ), 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"), "Information on the location of a hotspot.", append(commonHotspotLabels, "lng", "lat", "street", "state", "country", "city"), nil, ), prometheus.GaugeValue, } hotspotAntennaInfo = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "antenna_info"), "Information on the antenna of a hotspot.", append(commonHotspotLabels, "gain", "elevation"), nil, ), prometheus.GaugeValue, } hotspotActivity = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "activity_total"), "The total number of time an activity occurred in a hotspot.", append(commonHotspotLabels, "type"), nil, ), prometheus.CounterValue, } hotspotRewardsHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "rewards_hnt_total"), "The number of HNT token rewarded to a hotspot.", append(commonHotspotLabels, "type"), nil, ), prometheus.CounterValue, } ) func bool2Float64(b bool) float64 { if b { return 1.0 } return 0.0 } // Describe describes all the metrics ever exported by the helium blockchain exporter. // implements prometheus.Collector. func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- oraclePrice.Desc ch <- statsValidators.Desc ch <- statsOuis.Desc ch <- statsHotspotsDataOnly.Desc ch <- statsBlocks.Desc ch <- statsChallenges.Desc ch <- statsCities.Desc ch <- statsConsensusGroups.Desc ch <- statsCountries.Desc ch <- statsHotspots.Desc ch <- statsTokenSupply.Desc ch <- accountBalanceHnt.Desc ch <- accountActivity.Desc ch <- accountRewardsHnt.Desc ch <- accountDepositsHnt.Desc ch <- accountWithdrawalsHnt.Desc ch <- hotspotUp.Desc ch <- hotspotRelayed.Desc ch <- hotspotBlocksDelta.Desc ch <- hotspotBlocks.Desc ch <- hotspotStatusTimestamp.Desc ch <- hotspotRewardsScale.Desc ch <- hotspot5dWitnesses.Desc ch <- hotspot5dWitnessed.Desc ch <- hotspotGeocodeInfo.Desc ch <- hotspotAntennaInfo.Desc ch <- hotspotActivity.Desc ch <- hotspotRewardsHnt.Desc } // Collect fetches the data from the helium blockchain api and delivers them as Prometheus metrics. // implements prometheus.Collector. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { wg := new(sync.WaitGroup) wg.Add(2) go e.collectOracleMetrics(wg, ch) go e.collectStatsMetrics(wg, ch) for i := range e.Accounts { wg.Add(4) go e.collectAccountMetrics(wg, ch, &e.Accounts[i]) go e.collectAccountActivityMetrics(wg, ch, &e.Accounts[i]) go e.collectAccountTransactionsMetrics(wg, ch, &e.Accounts[i]) go e.collectHotspotMetrics(wg, ch, &e.Accounts[i]) } wg.Wait() } // collectOracleMetrics collect metrics in the oracle group from the helium api func (e *Exporter) collectOracleMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric) { defer wg.Done() currentOraclePrice, err := heliumapi.GetCurrentOraclePrice() if err != nil { log.Println(err) return } ch <- prometheus.MustNewConstMetric( oraclePrice.Desc, oraclePrice.Type, float64(currentOraclePrice.Price)/blockchain_oracle_factor, ) } // collectStatsMetrics collect metrics in the stats group from the helium api func (e *Exporter) collectStatsMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric) { defer wg.Done() blockchainStats, err := heliumapi.GetBlockchainStats() if err != nil { log.Println(err) return } ch <- prometheus.MustNewConstMetric( statsValidators.Desc, statsValidators.Type, float64(blockchainStats.Counts.Validators), ) ch <- prometheus.MustNewConstMetric( statsOuis.Desc, statsOuis.Type, float64(blockchainStats.Counts.Ouis), ) ch <- prometheus.MustNewConstMetric( statsHotspotsDataOnly.Desc, statsHotspotsDataOnly.Type, float64(blockchainStats.Counts.HotspotsDataonly), ) ch <- prometheus.MustNewConstMetric( statsBlocks.Desc, statsBlocks.Type, float64(blockchainStats.Counts.Blocks), ) ch <- prometheus.MustNewConstMetric( statsChallenges.Desc, statsChallenges.Type, float64(blockchainStats.Counts.Challenges), ) ch <- prometheus.MustNewConstMetric( statsCities.Desc, statsCities.Type, float64(blockchainStats.Counts.Cities), ) ch <- prometheus.MustNewConstMetric( statsConsensusGroups.Desc, statsConsensusGroups.Type, float64(blockchainStats.Counts.ConsensusGroups), ) ch <- prometheus.MustNewConstMetric( statsCountries.Desc, statsCountries.Type, float64(blockchainStats.Counts.Countries), ) ch <- prometheus.MustNewConstMetric( statsHotspots.Desc, statsHotspots.Type, float64(blockchainStats.Counts.Hotspots), ) ch <- prometheus.MustNewConstMetric( statsTokenSupply.Desc, statsTokenSupply.Type, blockchainStats.TokenSupply, ) } // collectStatsMetrics collect metrics in the account group from the helium api func (e *Exporter) collectAccountMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account) { defer wg.Done() accountForAddress, err := heliumapi.GetAccountForAddress(account.Address) if err != nil { log.Println(err) return } ch <- prometheus.MustNewConstMetric( accountBalanceHnt.Desc, accountBalanceHnt.Type, float64(accountForAddress.Balance)/blockchain_hnt_factor, account.Address, ) } // collectAccountActivityMetrics collect the total number of activities executed by an account from the helium api func (e *Exporter) collectAccountActivityMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account) { defer wg.Done() accountActivityForAddress, err := heliumapi.GetActivityCountsForAccount(account.Address) if err != nil { log.Println(err) return } for accType, count := range *accountActivityForAddress { ch <- prometheus.MustNewConstMetric( accountActivity.Desc, accountActivity.Type, float64(count), account.Address, accType, ) } } // collectAccountTransactionsMetrics collect the total deposited/withdrawn by an account from the helium api func (e *Exporter) collectAccountTransactionsMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account) { defer wg.Done() // We optimize our queries by asking the api only the activities we car about. activityTypes := []string{ "add_gateway_v1", "assert_location_v1", "assert_location_v2", "payment_v1", "payment_v2", "rewards_v1", "rewards_v2", "stake_validator_v1", "token_burn_v1", "transfer_hotspot_v1", "unstake_validator_v1", } // We can only want to allow a single instance of the routine doing // calculations on the deposited and widthdrawn total. account.UpdateLock.Lock() defer account.UpdateLock.Unlock() // We want to keep in memory the timestamp of the last activity we // received from the api. We cannot do something naive like [lastscrape, now] // because the api can take a few seconds to sync with the chain and // we can miss some activities by doing it that way. lastActivityTime := account.LastUpdate.Unix() updateLastActivityTime := func(newTime int64) { if lastActivityTime < newTime { lastActivityTime = newTime } } minTime := account.LastUpdate // Explicitly set max_time, to avoid issues with server-side caching maxTime := time.Now() activities, err := heliumapi.GetActivityForAccount(account.Address, activityTypes, &minTime, &maxTime) if err != nil { log.Println(err) return } // impl based on https://github.com/helium/hotspot-app/blob/918563fba84d1abf4554a43a4d42bb838d017bd3/src/features/wallet/root/useActivityItem.tsx#L336 for _, activity := range activities.AddGatewayV1 { if activity.Time > lastActivityTime { account.Tx.WithdrawalTotal += activity.StakingFee updateLastActivityTime(activity.Time) } } for _, activity := range activities.AssertLocationV1 { if activity.Time > lastActivityTime { account.Tx.WithdrawalTotal += activity.StakingFee updateLastActivityTime(activity.Time) } } for _, activity := range activities.AssertLocationV2 { if activity.Time > lastActivityTime { account.Tx.WithdrawalTotal += activity.StakingFee updateLastActivityTime(activity.Time) } } for _, activity := range activities.PaymentV1 { if activity.Time > lastActivityTime { if activity.Payer == account.Address { account.Tx.WithdrawalTotal += activity.Amount } else { account.Tx.DepositTotal += activity.Amount } updateLastActivityTime(activity.Time) } } for _, activity := range activities.PaymentV2 { if activity.Time > lastActivityTime { if activity.Payer == account.Address { paymentTotal := 0 for _, payment := range activity.Payments { paymentTotal += payment.Amount } account.Tx.WithdrawalTotal += paymentTotal } else { for _, payment := range activity.Payments { if payment.Payee == account.Address { account.Tx.DepositTotal += payment.Amount } } } updateLastActivityTime(activity.Time) } } for _, activity := range activities.RewardsV1 { if activity.Time > lastActivityTime { for _, reward := range activity.Rewards { account.RewardsTotal[reward.Type] += reward.Amount account.Tx.DepositTotal += reward.Amount } updateLastActivityTime(activity.Time) } } for _, activity := range activities.RewardsV2 { if activity.Time > lastActivityTime { for _, reward := range activity.Rewards { account.RewardsTotal[reward.Type] += reward.Amount account.Tx.DepositTotal += reward.Amount } updateLastActivityTime(activity.Time) } } for _, activity := range activities.StakeValidatorV1 { if activity.Time > lastActivityTime { account.Tx.WithdrawalTotal += activity.Stake updateLastActivityTime(activity.Time) } } for _, activity := range activities.TokenBurnV1 { if activity.Time > lastActivityTime { account.Tx.WithdrawalTotal += activity.Amount updateLastActivityTime(activity.Time) } } for _, activity := range activities.TransferHotspotV1 { if activity.Time > lastActivityTime { if activity.Buyer == account.Address { account.Tx.WithdrawalTotal += activity.AmountToSeller } else { account.Tx.DepositTotal += activity.AmountToSeller } updateLastActivityTime(activity.Time) } } for _, activity := range activities.UnstakeValidatorV1 { if activity.Time > lastActivityTime { account.Tx.WithdrawalTotal += activity.StakeAmount updateLastActivityTime(activity.Time) } } account.LastUpdate = time.Unix(lastActivityTime, 0) for rType, rTotal := range account.RewardsTotal { ch <- prometheus.MustNewConstMetric( accountRewardsHnt.Desc, accountRewardsHnt.Type, float64(rTotal)/blockchain_hnt_factor, account.Address, rType, ) } ch <- prometheus.MustNewConstMetric( accountDepositsHnt.Desc, accountDepositsHnt.Type, float64(account.Tx.DepositTotal)/blockchain_hnt_factor, account.Address, ) ch <- prometheus.MustNewConstMetric( accountWithdrawalsHnt.Desc, accountWithdrawalsHnt.Type, float64(account.Tx.WithdrawalTotal)/blockchain_hnt_factor, account.Address, ) } // collectStatsMetrics collect metrics of the hotspot of an account from the helium api func (e *Exporter) collectHotspotMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account) { defer wg.Done() hotspotsForAddress, err := heliumapi.GetHotspotsForAccount(account.Address) if err != nil { log.Println(err) return } for _, hotspotData := range hotspotsForAddress { statusTime, err := time.Parse(time.RFC3339Nano, hotspotData.Status.Timestamp) if err != nil { log.Println(err) continue } // create the hotspot object if it doesn't exist if _, ok := account.Hotspots[hotspotData.Address]; !ok { account.Hotspots[hotspotData.Address] = NewHotspot(hotspotData.Address) } // collect hotspot metric requiring extra queries in a new routine wg.Add(4) go e.collectHotspotBlocksDeltaMetrics(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) ch <- prometheus.MustNewConstMetric( hotspotUp.Desc, hotspotUp.Type, bool2Float64(hotspotData.Status.Online == "online"), account.Address, hotspotData.Address, hotspotData.Name, ) ch <- prometheus.MustNewConstMetric( hotspotRelayed.Desc, hotspotRelayed.Type, bool2Float64(len(hotspotData.Status.ListenAddrs) > 0 && strings.HasPrefix(hotspotData.Status.ListenAddrs[0], "/p2p")), account.Address, hotspotData.Address, hotspotData.Name, ) ch <- prometheus.MustNewConstMetric( hotspotBlocks.Desc, hotspotBlocks.Type, float64(hotspotData.Status.Height), account.Address, hotspotData.Address, hotspotData.Name, ) ch <- prometheus.MustNewConstMetric( hotspotStatusTimestamp.Desc, hotspotStatusTimestamp.Type, float64(statusTime.Unix()), account.Address, hotspotData.Address, hotspotData.Name, ) ch <- prometheus.MustNewConstMetric( hotspotRewardsScale.Desc, hotspotRewardsScale.Type, float64(hotspotData.RewardScale), account.Address, hotspotData.Address, hotspotData.Name, ) ch <- prometheus.MustNewConstMetric( hotspotGeocodeInfo.Desc, hotspotGeocodeInfo.Type, 1.0, account.Address, hotspotData.Address, hotspotData.Name, strconv.FormatFloat(hotspotData.Lng, 'f', 6, 64), strconv.FormatFloat(hotspotData.Lat, 'f', 6, 64), hotspotData.Geocode.LongStreet, hotspotData.Geocode.LongState, hotspotData.Geocode.LongCountry, hotspotData.Geocode.LongCity, ) ch <- prometheus.MustNewConstMetric( hotspotAntennaInfo.Desc, hotspotAntennaInfo.Type, 1.0, account.Address, hotspotData.Address, hotspotData.Name, strconv.Itoa(hotspotData.Gain), strconv.Itoa(hotspotData.Elevation), ) } } // collectHotspotBlocksDeltaMetrics calculate the gap between the block height of a hotstop and the height of the chain func (e *Exporter) collectHotspotBlocksDeltaMetrics(wg *sync.WaitGroup, ch chan<- prometheus.Metric, account *Account, hotspotData heliumapi.Hotspot) { defer wg.Done() delta := 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 } delta = float64(hotspotData.Status.Height - heightAtUpdate) } ch <- prometheus.MustNewConstMetric( hotspotBlocksDelta.Desc, hotspotBlocksDelta.Type, delta, 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.Hotspot) { defer wg.Done() hotspotActivityForAddress, err := heliumapi.GetHotspotActivityCounts(hotspotData.Address) if err != nil { log.Println(err) return } for accType, count := range *hotspotActivityForAddress { ch <- prometheus.MustNewConstMetric( hotspotActivity.Desc, hotspotActivity.Type, float64(count), account.Address, hotspotData.Address, hotspotData.Name, accType, ) } } // 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.Hotspot) { defer wg.Done() // We optimize our queries by asking the api only the activities we car about. activityTypes := []string{ "rewards_v1", "rewards_v2", } hotspot, ok := account.Hotspots[hotspotData.Address] if !ok { log.Fatalf("BUG: attempted to access hotspot %v before it was created", hotspotData.Address) return } hotspot.UpdateLock.Lock() defer hotspot.UpdateLock.Unlock() // We want to keep in memory the timestamp of the last activity we // received from the api. We cannot do something naive like [lastscrape, now] // because the api can take a few seconds to sync with the chain and // we can miss some activities by doing it that way. lastActivityTime := hotspot.LastUpdate.Unix() updateLastActivityTime := func(newTime int64) { if lastActivityTime < newTime { lastActivityTime = newTime } } minTime := hotspot.LastUpdate // Explicitly set max_time, to avoid issues with server-side caching maxTime := time.Now() activities, err := heliumapi.GetHotspotActivity(hotspot.Address, activityTypes, &minTime, &maxTime) if err != nil { log.Println(err) return } for _, activity := range activities.RewardsV1 { if activity.Time > lastActivityTime { for _, reward := range activity.Rewards { hotspot.RewardsTotal[reward.Type] += reward.Amount } updateLastActivityTime(activity.Time) } } for _, activity := range activities.RewardsV2 { if activity.Time > lastActivityTime { for _, reward := range activity.Rewards { hotspot.RewardsTotal[reward.Type] += reward.Amount } updateLastActivityTime(activity.Time) } } hotspot.LastUpdate = time.Unix(lastActivityTime, 0) for rType, rTotal := range hotspot.RewardsTotal { ch <- prometheus.MustNewConstMetric( hotspotRewardsHnt.Desc, hotspotRewardsHnt.Type, float64(rTotal)/blockchain_hnt_factor, account.Address, hotspot.Address, hotspotData.Name, rType, ) } account.Hotspots[hotspot.Address] = hotspot } func main() { var err error fApiUrl := flag.String("apiUrl", "https://helium-api.stakejoy.com", "The helium api url") fApiTimeout := flag.String("apiTimeout", "10s", "The request timeout to the helium api.") fApiRateLimit := flag.Int("maxApiRate", 10, "The maximum number of helium api calls per seconds.") fHeliumAccounts := flag.String("accounts", "", "A comma-delimited list of helium accounts to scrape (optional)") fMetricsPath := flag.String("metricpath", "/metrics", "The metrics path") fListenAddress := flag.String("listenAddress", "0.0.0.0", "The http server listen address") fListenPort := flag.String("listenPort", "9865", "The http server listen port") flag.Parse() heliumapi.ApiUrl = *fApiUrl heliumapi.Limiter = rate.NewLimiter(rate.Every(time.Duration(1000/(*fApiRateLimit))*time.Millisecond), *fApiRateLimit) heliumapi.ApiTimeout, err = time.ParseDuration(*fApiTimeout) if err != nil { log.Fatalf("failed to parse apiTimeout: %s", err.Error()) } heliumAccounts := strings.Split(*fHeliumAccounts, ",") serverAddr := *fListenAddress + ":" + *fListenPort e, err := NewExporter(heliumAccounts) if err != nil { log.Fatalf("failed to start exporter: %s", err.Error()) } r := prometheus.NewRegistry() r.MustRegister(e) // setup http route http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(` Helium blockchain exporter

Helium blockchain exporter

Metrics

`)) }) http.Handle(*fMetricsPath, promhttp.HandlerFor(r, promhttp.HandlerOpts{})) log.Printf("listening on %v\n", serverAddr) if err = http.ListenAndServe(serverAddr, nil); err != nil { log.Println(err) os.Exit(1) } }