package main import ( "flag" "fmt" "log" "net/http" "strconv" "strings" "time" "github.com/helium-blockchain-exporter/heliumapi" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) // 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 Tx AccountTx } func NewAccount(address string) Account { return Account{ Address: address, Tx: AccountTx{ DepositTotal: 0, WithdrawalTotal: 0, LastUpdate: time.Now(), // LastUpdate: time.Time{}, }, } } type AccountTx struct { DepositTotal int WithdrawalTotal int LastUpdate time.Time } const ( namespace = "helium" ) var ( // Activity classification accountDepositActivities = []string{ "payment_v1", // if payee == account_address "payment_v2", // if payee == account_address "rewards_v1", "unstake_validator_v1", } accountWithdrawalActivities = []string{ "add_gateway_v1", "assert_location_v1", "assert_location_v2", "payment_v1", // if payer == account_address "payment_v2", // if payer == account_address "stake_validator_v1", "token_burn_v1", } // 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 total height/number of blocks in 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.", commonAccountLabels, nil, ), prometheus.CounterValue, } accountDepositsHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "deposits_hnt_total"), "The number of HNT tokens deposited to this account.", commonAccountLabels, nil, ), prometheus.CounterValue, } accountWithdrawalsHnt = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "account", "withdrawals_hnt_total"), "The number of HNT tokens withdrawn from this 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_total"), "The block height of a hotspot. Check on the hotspot itself for the most recent data.", commonHotspotLabels, nil, ), prometheus.CounterValue, } hotspotRewardsScale = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "hotspot", "rewards_scale"), "The reward scale of a hotspot.", 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.", commonHotspotLabels, 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 <- hotspotBlocks.Desc ch <- hotspotRewardsScale.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) { e.collectOracleMetrics(ch) e.collectStatsMetrics(ch) for i := range e.Accounts { e.collectAccountMetrics(ch, &e.Accounts[i]) e.collectHotspotMetrics(ch, &e.Accounts[i]) } } // collectOracleMetrics collect metrics in the oracle group from the helium api func (e *Exporter) collectOracleMetrics(ch chan<- prometheus.Metric) { currentOraclePrice, err := heliumapi.GetCurrentOraclePrice() if err != nil { fmt.Println(err) return } ch <- prometheus.MustNewConstMetric( oraclePrice.Desc, oraclePrice.Type, float64(currentOraclePrice.Data.Price)/100000000, ) } // collectStatsMetrics collect metrics in the stats group from the helium api func (e *Exporter) collectStatsMetrics(ch chan<- prometheus.Metric) { blockchainStats, err := heliumapi.GetBlockchainStats() if err != nil { fmt.Println(err) return } ch <- prometheus.MustNewConstMetric( statsValidators.Desc, statsValidators.Type, float64(blockchainStats.Data.Counts.Validators), ) ch <- prometheus.MustNewConstMetric( statsOuis.Desc, statsOuis.Type, float64(blockchainStats.Data.Counts.Ouis), ) ch <- prometheus.MustNewConstMetric( statsHotspotsDataOnly.Desc, statsHotspotsDataOnly.Type, float64(blockchainStats.Data.Counts.HotspotsDataonly), ) ch <- prometheus.MustNewConstMetric( statsBlocks.Desc, statsBlocks.Type, float64(blockchainStats.Data.Counts.Blocks), ) ch <- prometheus.MustNewConstMetric( statsChallenges.Desc, statsChallenges.Type, float64(blockchainStats.Data.Counts.Challenges), ) ch <- prometheus.MustNewConstMetric( statsCities.Desc, statsCities.Type, float64(blockchainStats.Data.Counts.Cities), ) ch <- prometheus.MustNewConstMetric( statsConsensusGroups.Desc, statsConsensusGroups.Type, float64(blockchainStats.Data.Counts.ConsensusGroups), ) ch <- prometheus.MustNewConstMetric( statsCountries.Desc, statsCountries.Type, float64(blockchainStats.Data.Counts.Countries), ) ch <- prometheus.MustNewConstMetric( statsHotspots.Desc, statsHotspots.Type, float64(blockchainStats.Data.Counts.Hotspots), ) ch <- prometheus.MustNewConstMetric( statsTokenSupply.Desc, statsTokenSupply.Type, blockchainStats.Data.TokenSupply, ) } // collectStatsMetrics collect metrics in the account group from the helium api func (e *Exporter) collectAccountMetrics(ch chan<- prometheus.Metric, account *Account) { accountForAddress, err := heliumapi.GetAccountForAddress(account.Address) if err != nil { fmt.Println(err) return } accountActivityForAddress, err := heliumapi.GetActivityCountsForAccount(account.Address) if err != nil { fmt.Println(err) return } accountRewardTotalsForAddress, err := heliumapi.GetRewardTotalsForAccount(account.Address, &e.StartTime, nil) if err != nil { fmt.Println(err) return } tx, err := account.computeTransactions() if err != nil { fmt.Println(err) return } ch <- prometheus.MustNewConstMetric( accountBalanceHnt.Desc, accountBalanceHnt.Type, float64(accountForAddress.Data.Balance), account.Address, ) for accType, count := range accountActivityForAddress.Data { ch <- prometheus.MustNewConstMetric( accountActivity.Desc, accountActivity.Type, float64(count), account.Address, accType, ) } ch <- prometheus.MustNewConstMetric( accountRewardsHnt.Desc, accountRewardsHnt.Type, accountRewardTotalsForAddress.Data.Sum, account.Address, ) ch <- prometheus.MustNewConstMetric( accountDepositsHnt.Desc, accountDepositsHnt.Type, float64(tx.DepositTotal), account.Address, ) ch <- prometheus.MustNewConstMetric( accountWithdrawalsHnt.Desc, accountWithdrawalsHnt.Type, float64(tx.WithdrawalTotal), account.Address, ) } // collectStatsMetrics collect metrics in the hotspot group from the helium api func (e *Exporter) collectHotspotMetrics(ch chan<- prometheus.Metric, account *Account) { hotspotsForAddress, err := heliumapi.GetHotspotsForAccount(account.Address) if err != nil { fmt.Println(err) return } for _, hotspotData := range hotspotsForAddress.Data { hotspotActivityForAddress, err := heliumapi.GetHotspotActivityCount(hotspotData.Address) if err != nil { fmt.Println(err) return } hotspotRewardTotalsForAddress, err := heliumapi.GetRewardsTotalForHotspot(hotspotData.Address, &e.StartTime, nil) if err != nil { fmt.Println(err) return } 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( 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), ) for accType, count := range hotspotActivityForAddress.Data { ch <- prometheus.MustNewConstMetric( hotspotActivity.Desc, hotspotActivity.Type, float64(count), account.Address, hotspotData.Address, hotspotData.Name, accType, ) } ch <- prometheus.MustNewConstMetric( hotspotRewardsHnt.Desc, hotspotRewardsHnt.Type, hotspotRewardTotalsForAddress.Data.Sum, account.Address, hotspotData.Address, hotspotData.Name, ) } } func (a *Account) computeTransactions() (*AccountTx, error) { now := time.Now() _, err := heliumapi.GetActivityForAccount(a.Address, []string{}, &a.Tx.LastUpdate, &now) if err != nil { return nil, err } // fmt.Println(activities) a.Tx.LastUpdate = now return &a.Tx, nil } func main() { fHeliumAccounts := flag.String("accounts", "", "A comma-delimited list of helium accounts to scrape.") 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", "9111", "The http server listen port") flag.Parse() 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{})) fmt.Printf("listening on %v\n", serverAddr) http.ListenAndServe(serverAddr, nil) }