package main import ( "flag" "fmt" "log" "net/http" "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 } // 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 ( namespace = "helium" ) var ( // lables 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_total"), "The total number of validators.", nil, nil, ), prometheus.CounterValue, } statsOuis = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "ouis_total"), "The total number of organization unique identifiers.", nil, nil, ), prometheus.CounterValue, } statsHotspotsDataOnly = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "hotspots_dataonly_total"), "The total number of data only hotspots.", nil, nil, ), prometheus.CounterValue, } statsBlocks = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "blocks_total"), "The total height/number of blocks in the blockchain.", nil, nil, ), prometheus.CounterValue, } statsChallenges = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "challenges_total"), "The total number of challenges.", nil, nil, ), prometheus.CounterValue, } 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_total"), "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_total"), "The total number of hotspots.", nil, nil, ), prometheus.CounterValue, } statsTokenSupply = metricInfo{ prometheus.NewDesc( prometheus.BuildFQName(namespace, "stats", "token_supply"), "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 the 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"), "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, // } // helium hotspot metrics ) // 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 } // 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 _, account := range e.Accounts { e.collectAccountMetrics(ch, &account) } } // 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.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 } ch <- prometheus.MustNewConstMetric( accountBalanceHnt.Desc, accountBalanceHnt.Type, float64(accountForAddress.Data.Balance), 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(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") 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) }